Deploy to production
Goal: get a Loco app running on a production host. Loco compiles to a single self-contained binary — the target server needs neither cargo nor a Rust toolchain, just the binary and a config/ folder.
1. Build the release binary
Section titled “1. Build the release binary”cargo build --releaseYour binary name matches the [package] name in Cargo.toml (with a -cli suffix, e.g. myapp-cli), and lands in ./target/release/.
2. Generate a Dockerfile (optional)
Section titled “2. Generate a Dockerfile (optional)”cargo loco generate deployment dockerkind is a positional argument — docker, nginx, or lambda, not a --kind flag.
This writes two files to your project root:
Dockerfile— multi-stage build: compiles withcargo build --releasein arust:slimbuilder stage, then copies just the compiled binary andconfig/into a slimdebian:bookworm-slimruntime image. If your app has afrontend/package.json(client-side rendering), it also installs Node and runsnpm install && npm run buildin the builder stage. Ifserver.middlewares.static_assetsis configured, the folders it points to are copied into the final image too..dockerignore— excludestarget/,.git, and other build artifacts from the Docker build context.
Build and run it like any other image:
docker build -t myapp .docker run -p 5150:5150 --env-file .env myapp3. Generate an nginx config (optional)
Section titled “3. Generate an nginx config (optional)”cargo loco generate deployment nginxThis writes nginx/default.conf, a reverse-proxy config derived from your current server.host / server.port (config/<env>.yaml) — it proxies both the bare domain and wildcard subdomains to your app.
4. Deploy to AWS Lambda (optional)
Section titled “4. Deploy to AWS Lambda (optional)”cargo loco generate deployment lambdaLoco builds a standard Axum Router, and both Axum and the AWS Lambda runtime are tower::Services — so your app runs on Lambda with no rewrite. This writes:
src/bin/lambda.rs— a Lambda entrypoint that boots your app inServerOnlymode and hands the router to the Lambda HTTP runtime (lambda_http::run). It’s a separate binary target, socargo loco startand your CLI are untouched.- adds
lambda_httpto yourCargo.toml. - writes a
[package.metadata.lambda]block to yourCargo.tomlso build/deploy need no extra flags — it declares which runtime files ship in the zip (config/, plusassets/etc. when detected) and sensible deploy defaults (memory,timeout,LOCO_ENV=production). Nothing environment-specific is baked in — region, account, IAM role and secrets are supplied at deploy time.
Then deploy with cargo-lambda — two commands, no flags:
cargo install cargo-lambdacargo lambda build --release --arm64 --output-format zipcargo lambda deploy --enable-function-urldeploy creates the function, an execution role, and a public Function URL, then prints the HTTPS endpoint. Set secrets with cargo lambda deploy --enable-function-url --env-var DATABASE_URL=... --env-var JWT_SECRET=... (or wire Secrets Manager).
The deliverable
Section titled “The deliverable”cargo lambda build produces a .zip under target/lambda/lambda/ containing a single bootstrap executable (your compiled Rust binary) plus the runtime files declared in the metadata include. That zip is the entire artifact submitted to AWS — there’s no managed runtime layer; it runs on the provided.al2023 custom runtime. Measured for a stock db app: ~18 MB unzipped → ~8 MB zip — well under Lambda’s 50 MB zipped / 250 MB unzipped direct-upload limit, so no S3 staging. Prefer --arm64 (Graviton) for lower cost and faster cold starts.
What ships beyond the binary: anything Loco reads from disk at runtime — always config/, plus assets/, i18n files, and src/mailers/ templates if your app serves views/static assets/i18n/mail. The generator detects these and lists them in the metadata include; add more entries there if you read other files at runtime. (Alternatively, containerize — see below — which packages the whole app dir.)
What this touches on the AWS side
Section titled “What this touches on the AWS side”Beyond the function itself, a working deploy involves:
- A front door. A Lambda Function URL or an API Gateway (v2 HTTP API).
lambda_httphandles all three event shapes (Function URL, API GW v1/v2) transparently. - An IAM execution role. CloudWatch Logs permissions at minimum (
cargo lambda deploycan create a basic role, or pass--role); add VPC-access permissions if you attach to a VPC, plus permissions for anything the app calls (S3, SES, Secrets Manager). - Logs. Loco’s tracing output goes to stdout → CloudWatch Logs. Use JSON logging in production.
- Config & secrets. Set
LOCO_ENVand secrets (DATABASE_URL, JWT secret, …) as function env vars (--env-var) or via Secrets Manager/SSM. - Networking to a database. To reach RDS in a VPC, attach the function to the VPC’s subnets + a security group (needs the VPC-access role). Outbound calls to SES/S3/Secrets Manager then need a NAT gateway or VPC endpoints. Strongly consider RDS Proxy: each warm Lambda instance holds its own DB connections, so scaling can exhaust Postgres connection limits — RDS Proxy pools them.
Notes & limits
Section titled “Notes & limits”- HTTP only. Background workers and the scheduler aren’t started in the Lambda entrypoint — they don’t fit Lambda’s request/response model. Run those on an always-on target (ECS/EC2), or drive them from SQS/EventBridge.
- Keep migrations out of the request path. Run
cargo loco db migratefrom CI or a one-off task, and point the runtimeLOCO_ENVat a config that doesn’t auto-migrate on boot. - Cold starts. Rust starts fast, but Loco boots the whole app (including DB connect) per cold start; VPC attachment adds ENI setup latency. Provisioned concurrency smooths this if needed.
- Prefer zero code changes? You can instead containerize your normal binary with the AWS Lambda Web Adapter on top of the generated
Dockerfile— nolambda.rsneeded, and the whole app dir (config, assets) ships in the image.
5. Set the environment variables production requires
Section titled “5. Set the environment variables production requires”There’s no separate “production mode” — Loco picks a config file by environment, and the default environment is development. Set LOCO_ENV=production on the server (or pass --environment production), or your app will read config/development.yaml there and appear to work.
config/production.yaml was written for you at loco new, already tuned for production: backtraces off, json logs, 0.0.0.0 binding, a real connection pool.
What it deliberately does not contain is any secret or address. Those read from the environment with no fallback:
| Variable | Used for | Required |
|---|---|---|
DATABASE_URL |
Database connection | yes, with a database |
JWT_SECRET |
Signing and verifying tokens | yes, with auth |
HOST |
The public URL that links in outgoing mail point to | yes |
MAILER_HOST, MAILER_USER, MAILER_PASSWORD |
SMTP server and credentials | yes, with a mailer |
REDIS_URL / QUEUE_URL |
Queue backend | yes, with a queue |
PORT, BINDING, LOG_LEVEL, DB_MAX_CONNECTIONS, DB_AUTO_MIGRATE |
Overrides | no, sensible defaults |
A missing required variable stops the app at startup with the variable’s name, rather than letting it run with a development secret or point at a database that isn’t there. That is intentional: a boot failure you can read is better than an app that appears healthy and is signing tokens with a key committed to your repository.
export DATABASE_URL='postgres://user:password@db-host:5432/myapp_production'export JWT_SECRET="$(openssl rand -hex 32)"export HOST='https://myapp.example.com'If you run more than one instance, or migrate as a separate release step, set DB_AUTO_MIGRATE=false so instances don’t race to migrate the same database.
See Configure logging for logging, and the Configuration reference for every key in the file.
6. Run loco doctor before going live
Section titled “6. Run loco doctor before going live”myapp-cli doctor --environment productionRun this on the server, where the environment variables and the database it will actually use are. doctor opens the production config, connects to the DB and queue it names, and additionally reports settings that are safe in development and not in production — a loopback binding, dangerously_truncate, backtraces left on.
Add -c/--config to print the fully-resolved config, after environment substitution, for inspection:
myapp-cli doctor --config --environment production--production still works as a deprecated alias for --environment production.
7. Ship it
Section titled “7. Ship it”Copy the binary and the config/ folder to the server (no source, no Cargo.lock, no toolchain needed):
scp target/release/myapp-cli config/ user@server:/opt/myapp/ssh user@server 'LOCO_ENV=production /opt/myapp/myapp-cli start'Verify
Section titled “Verify”myapp-cli doctor --environment productionexits 0 and reports all checks passing.myapp-cli startboots and the startup banner shows the environment, DB, and logger you expect.- Hitting the app’s health/root route through nginx (if you generated one) returns a response, confirming the reverse proxy is wired to the right host/port.
Reference
Section titled “Reference”generate deploymentCLI shape (docker/nginx/lambdaaskind): CLI reference- Every config key referenced above (
logger,server,database,auth,mailer,queue): Configuration reference