|
1 | 1 | #pragma warning disable ASPIREPIPELINES003 // WithDeploymentImageTag is experimental |
2 | | -#pragma warning disable ASPIRECOMPUTE003 // ContainerRegistryResource is experimental |
3 | 2 |
|
4 | | -// ============================================================================= |
5 | 3 | // Sample: SSH Deployment to Remote Docker Host |
6 | | -// ============================================================================= |
| 4 | +// Demonstrates: custom image tagging, YARP reverse proxy, optional HTTPS with Let's Encrypt |
7 | 5 | // |
8 | | -// This sample demonstrates deploying an Aspire application to a remote Docker |
9 | | -// host via SSH with the following features: |
10 | | -// |
11 | | -// - Custom image tagging from CI/CD pipelines |
12 | | -// - Container registry integration with automatic login |
13 | | -// - YARP reverse proxy as the public entry point |
14 | | -// - Optional HTTPS with automatic Let's Encrypt certificate generation |
15 | | -// |
16 | | -// Configuration (appsettings.json or environment variables): |
17 | | -// ---------------------------------------------------------- |
18 | | -// - EnableHttps: Set to "true" to enable HTTPS with Let's Encrypt certificates. |
19 | | -// When false (default), only HTTP on port 80 is exposed. |
20 | | -// |
21 | | -// Required parameters for deployment: |
22 | | -// - Parameters__registry-endpoint: Registry URL (e.g., "registry.digitalocean.com") |
23 | | -// - Parameters__registry-repository: Repository prefix (e.g., "my-project") |
24 | | -// - Parameters__registry-username: Registry username |
25 | | -// - Parameters__registry-password: Registry password (secret) |
26 | | -// |
27 | | -// Required parameters when EnableHttps=true: |
28 | | -// - Parameters__domain: The domain name for the certificate (e.g., "example.com") |
29 | | -// - Parameters__letsencrypt_email: Email for Let's Encrypt registration/notifications |
30 | | -// |
31 | | -// How container registry integration works: |
32 | | -// ----------------------------------------- |
33 | | -// 1. A ContainerRegistryResource is created with endpoint and repository |
34 | | -// 2. WithCredentialsLogin adds a login step that runs before push-prereq |
35 | | -// 3. Built-in push steps (push-apiservice, push-webfrontend) push to the registry |
36 | | -// 4. Remote deployment authenticates with the same credentials to pull images |
37 | | -// |
38 | | -// How HTTPS works: |
39 | | -// ---------------- |
40 | | -// When EnableHttps is true, a certbot container is added that: |
41 | | -// 1. Runs the Let's Encrypt ACME HTTP-01 challenge on port 80 |
42 | | -// 2. Obtains/renews certificates and stores them in a shared Docker volume |
43 | | -// 3. Sets permissions so non-root containers can read the certificates |
44 | | -// 4. Exits after certificate generation |
45 | | -// |
46 | | -// YARP then starts (after certbot completes) and: |
47 | | -// - Mounts the shared certificate volume (read-only) |
48 | | -// - Serves HTTPS on port 443 using the Let's Encrypt certificates |
49 | | -// - Serves HTTP on port 80 |
50 | | -// |
51 | | -// On subsequent deployments, certbot skips certificate generation if valid |
52 | | -// certificates already exist (--keep-until-expiring flag). |
53 | | -// |
54 | | -// ============================================================================= |
| 6 | +// Key settings: |
| 7 | +// - IMAGE_TAG_SUFFIX: Custom image tag (e.g., "build.42.abc1234") |
| 8 | +// - EnableHttps: Enable HTTPS with Let's Encrypt (requires domain + email parameters) |
| 9 | +// - DockerRegistry__*: Registry URL, prefix, and optional credentials |
55 | 10 |
|
56 | 11 | var builder = DistributedApplication.CreateBuilder(args); |
57 | 12 |
|
58 | | -// Custom image tag from CI/CD (e.g., IMAGE_TAG_SUFFIX=build.42.abc1234) |
59 | 13 | var imageTag = builder.Configuration["IMAGE_TAG_SUFFIX"]; |
60 | 14 |
|
61 | | -// Container registry configuration for push/pull operations |
62 | | -// These parameters are prompted during deployment or can be set via config |
63 | | -var registryEndpoint = builder.AddParameter("registry-endpoint"); |
64 | | -var registryRepository = builder.AddParameter("registry-repository"); |
65 | | - |
66 | | -var registryUsername = builder.AddParameter("registry-username"); |
67 | | -var registryPassword = builder.AddParameter("registry-password", secret: true); |
68 | | - |
69 | | -// Create container registry with automatic login |
70 | | -// This creates a login-to-registry step that runs before built-in push steps |
71 | | -var registry = builder.AddContainerRegistry("registry", registryEndpoint, registryRepository) |
72 | | - .WithCredentialsLogin(registryUsername, registryPassword); |
73 | | - |
74 | | -// Configure Docker Compose environment with SSH deployment support |
| 15 | +// Configure Docker Compose environment with SSH deployment |
| 16 | +// Registry is automatically configured from DockerRegistry:* settings |
75 | 17 | builder.AddDockerComposeEnvironment("env") |
76 | 18 | .WithDashboard(db => db.WithHostPort(8085)) |
77 | | - .WithSshDeploySupport() |
78 | | - .WithContainerRegistry(registry); |
| 19 | + .WithSshDeploySupport(); |
79 | 20 |
|
80 | 21 | var p = builder.AddParameter("p"); |
81 | 22 |
|
|
99 | 40 | c.AddRoute(frontend); |
100 | 41 | }); |
101 | 42 |
|
102 | | -// Apply the image tag to all services if specified |
| 43 | +// Apply custom image tag from CI/CD to all services |
103 | 44 | if (!string.IsNullOrEmpty(imageTag)) |
104 | 45 | { |
105 | 46 | apiService.WithImagePushOptions(c => c.Options.RemoteImageTag = imageTag); |
|
113 | 54 |
|
114 | 55 | if (enableHttps) |
115 | 56 | { |
116 | | - // Let's Encrypt parameters (required when EnableHttps=true) |
117 | | - // Set via: Parameters__domain and Parameters__letsencrypt_email |
118 | 57 | var domain = builder.AddParameter("domain"); |
119 | 58 | var letsEncryptEmail = builder.AddParameter("letsencrypt-email"); |
120 | 59 |
|
121 | | - // Certbot container for automatic Let's Encrypt certificate generation |
122 | | - // Uses the official certbot/certbot image to obtain and renew certificates. |
123 | | - // |
124 | | - // Certbot arguments explained: |
125 | | - // - certonly: Only obtain the certificate, don't install it |
126 | | - // - --standalone: Run a temporary webserver for the ACME HTTP-01 challenge |
127 | | - // - --non-interactive: Don't prompt for user input |
128 | | - // - --agree-tos: Agree to Let's Encrypt Terms of Service |
129 | | - // - --keep-until-expiring: Skip if cert exists and is not within 30 days of expiry |
130 | | - // - --deploy-hook: Command to run after successful cert issuance (fix permissions) |
131 | | - // - --email: Email for urgent renewal/security notices |
132 | | - // - -d: Domain name for the certificate |
133 | | - // |
| 60 | + // Certbot container for automatic Let's Encrypt certificate generation. |
| 61 | + // How it works: |
| 62 | + // 1. Certbot starts and binds to port 80 for the ACME HTTP-01 challenge |
| 63 | + // 2. Let's Encrypt verifies domain ownership by requesting /.well-known/acme-challenge/ |
| 64 | + // 3. Certificates are stored in a shared Docker volume (/etc/letsencrypt) |
| 65 | + // 4. Certbot fixes permissions so non-root containers can read the certs, then exits |
| 66 | + // 5. On subsequent deploys, --keep-until-expiring skips renewal if certs are still valid |
134 | 67 | var certbot = builder.AddContainer("certbot", "certbot/certbot") |
135 | | - // Shared volume for certificates - both certbot and YARP mount this |
136 | 68 | .WithVolume("letsencrypt", "/etc/letsencrypt") |
137 | | - // Port 80 must be published to host for Let's Encrypt to reach the ACME challenge |
138 | 69 | .WithHttpEndpoint(port: 80, targetPort: 80) |
139 | 70 | .WithExternalHttpEndpoints() |
140 | 71 | .WithArgs(context => |
|
145 | 76 | context.Args.Add("--agree-tos"); |
146 | 77 | context.Args.Add("-v"); |
147 | 78 | context.Args.Add("--keep-until-expiring"); |
148 | | - // Fix permissions so non-root containers (like YARP) can read the certs |
149 | 79 | context.Args.Add("--deploy-hook"); |
150 | 80 | context.Args.Add("chmod -R 755 /etc/letsencrypt/live && chmod -R 755 /etc/letsencrypt/archive"); |
151 | 81 | context.Args.Add("--email"); |
|
154 | 84 | context.Args.Add(domain.Resource); |
155 | 85 | }); |
156 | 86 |
|
157 | | - // Certbot and YARP both need port 80, but at different times: |
158 | | - // - Certbot needs port 80 during the ACME challenge (runs first, then exits) |
159 | | - // - YARP needs port 80 for HTTP traffic (starts after certbot completes) |
160 | | - // WaitForCompletion ensures YARP doesn't start until certbot exits successfully |
| 87 | + // YARP waits for certbot to complete since both need port 80. |
| 88 | + // Certbot runs the ACME challenge first, then YARP takes over for traffic. |
161 | 89 | yarp.WaitForCompletion(certbot); |
162 | | - |
163 | 90 | yarp.WithHostPort(80); |
164 | 91 | yarp.WithHttpsEndpoint(443); |
165 | | - |
166 | | - // Mount the shared certificate volume (read-only since YARP only reads certs) |
167 | 92 | yarp.WithVolume("letsencrypt", "/etc/letsencrypt", isReadOnly: true); |
168 | 93 |
|
169 | 94 | // Configure Kestrel to use the Let's Encrypt certificates |
170 | | - // Certbot stores certs at: /etc/letsencrypt/live/{domain}/ |
171 | 95 | yarp.WithEnvironment(context => |
172 | 96 | { |
173 | 97 | context.EnvironmentVariables["Kestrel__Certificates__Default__Path"] = |
174 | 98 | ReferenceExpression.Create($"/etc/letsencrypt/live/{domain}/fullchain.pem"); |
175 | 99 | context.EnvironmentVariables["Kestrel__Certificates__Default__KeyPath"] = |
176 | 100 | ReferenceExpression.Create($"/etc/letsencrypt/live/{domain}/privkey.pem"); |
177 | 101 |
|
178 | | - // Configure URLs for both HTTP and HTTPS |
179 | 102 | var httpEndpoint = yarp.GetEndpoint("http"); |
180 | 103 | var httpsEndpoint = yarp.GetEndpoint("https"); |
181 | 104 |
|
|
189 | 112 | } |
190 | 113 | else |
191 | 114 | { |
192 | | - // HTTP only |
| 115 | + // HTTP only mode - expose YARP on port 80 |
193 | 116 | yarp.WithHostPort(80); |
194 | 117 | } |
195 | 118 |
|
|
0 commit comments