Skip to content
loco
v1.0
★ 6.9k Get started

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

Terminal window
cargo build --release

Your 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)

Terminal window
cargo loco generate deployment docker

kind is a positional argument — docker or nginx, not a --kind flag.

This writes two files to your project root:

  • Dockerfile — multi-stage build: compiles with cargo build --release in a rust:slim builder stage, then copies just the compiled binary and config/ into a slim debian:bookworm-slim runtime image. If your app has a frontend/package.json (client-side rendering), it also installs Node and runs npm install && npm run build in the builder stage. If server.middlewares.static_assets is configured, the folders it points to are copied into the final image too.
  • .dockerignore — excludes target/, .git, and other build artifacts from the Docker build context.

Build and run it like any other image:

Terminal window
docker build -t myapp .
docker run -p 5150:5150 --env-file .env myapp

3. Generate an nginx config (optional)

Terminal window
cargo loco generate deployment nginx

This 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. Review production config

There’s no separate “production mode” — Loco picks a config file by environment (config/production.yaml by default, or override with LOCO_ENV). Before deploying, walk through these sections:

Logger — turn pretty_backtrace off (it’s development-friendly, not performance-friendly) and prefer json for log aggregation:

logger:
enable: true
pretty_backtrace: false
level: info
format: json

See Configure logging for the full picture.

Server — bind to all interfaces and inject the port from the environment:

server:
port: {{ get_env(name="NODE_PORT", default=5150) }}
host: {{ get_env(name="APP_HOST", default="http://localhost") }}

Database — real connection limits, no destructive flags:

database:
uri: "{{ get_env(name='DATABASE_URL', default='postgres://loco:loco@localhost:5432/loco_app') }}"
enable_logging: false
connect_timeout: 500
idle_timeout: 500
min_connections: 1
max_connections: 10
auto_migrate: true
dangerously_truncate: false
dangerously_recreate: false

Auth secret — inject via environment, never hardcode:

auth:
jwt:
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800

Queue / mailer — same pattern: point uri/host at env vars. See the Configuration reference for every key across all of these sections.

5. Run loco doctor before going live

Terminal window
myapp-cli doctor --production

doctor validates DB/cache/queue connectivity against the config it would actually load. Add -c/--config to also print the fully-resolved config for inspection:

Terminal window
myapp-cli doctor --config --production

6. Ship it

Copy the binary and the config/ folder to the server (no source, no Cargo.lock, no toolchain needed):

Terminal window
scp target/release/myapp-cli config/ user@server:/opt/myapp/
ssh user@server '/opt/myapp/myapp-cli start'

Verify

  • myapp-cli doctor --production exits 0 and reports all checks passing.
  • myapp-cli start boots 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