Marten + SQLite + Assets CDN
Status: verified example based on an anonymized production deployment.
Small Marten deployment with no database accessory. SQLite lives on a named Podman volume, while fingerprinted static assets are published on a separate hostname.
.meridian/deploy.yml
service: my-app
image: localhost/my-app:latest
servers:
web:
hosts:
- prod-01.example.com
proxy:
host: app.example.com
ssl: true
app_port: 8000
healthcheck:
path: /health
env:
clear:
MARTEN_ENV: production
MARTEN_ALLOWED_HOSTS: app.example.com
secret:
- MARTEN_SECRET_KEY
ssh:
user: deploy
keys:
- ~/.ssh/id_ed25519
transfer:
mode: stream
volumes:
- my-app-data:/app/data
assets:
host: assets.app.example.com
command: /app/manage collectassets --fingerprint --no-input
output_dir: /app/assets
retain_releases: 2
hooks:
remote:
before_start:
- command: >-
podman run --rm
-v my-app-data:/app/data
-e MARTEN_ENV=production
-e MARTEN_ALLOWED_HOSTS=app.example.com
--secret MARTEN_SECRET_KEY,type=env,target=MARTEN_SECRET_KEY
localhost/my-app:latest /app/manage migrateThe migration container mounts the same volume as the web container, so schema changes are applied to the persistent database before the new app color starts. See volumes, assets, and hooks. The binary and asset workflow follows Marten's deployment and asset handling guidance. Marten's config.assets.url (in the production settings below) must point at the same host as assets.host so fingerprinted URLs resolve to the generated asset server; see CSS url() 404s if CSS-referenced files 404.
Containerfile
FROM crystallang/crystal:1.20.2-alpine AS build
RUN apk --no-cache add sqlite-dev openssl-dev yaml-dev zlib-dev \
openssl-libs-static yaml-static zlib-static sqlite-static
ENV MARTEN_ENV=production
ENV SKIP_MARTEN_CLI_PRECOMPILATION=true
WORKDIR /app
COPY shard.yml shard.lock ./
RUN shards install --production --skip-postinstall
COPY . .
RUN mkdir -p bin && \
crystal build src/server.cr -o bin/server --static && \
crystal build manage.cr -o bin/manage --static
# The web process needs the generated manifest at runtime to resolve
# fingerprinted URLs. The deploy asset builder publishes the same files.
RUN MARTEN_SECRET_KEY=build-only \
MARTEN_ALLOWED_HOSTS=build.example \
bin/manage collectassets --fingerprint --no-input
FROM alpine:3.21
RUN apk add --no-cache tzdata
ENV MARTEN_ENV=production
WORKDIR /app
RUN mkdir -p /app/data
COPY --from=build /app/bin/server /app/server
COPY --from=build /app/bin/manage /app/manage
COPY --from=build /app/src/ /app/src/
COPY --from=build /app/assets/ /app/assets/
EXPOSE 8000
CMD ["/app/server"]Marten Production Snippets
The base database configuration points SQLite at the mounted volume:
Marten.configure do |config|
config.database do |db|
db.backend = :sqlite
db.name = Path["/app/data/my-app.db"]
end
endProduction settings load secrets, trust the proxy headers, and map Marten's asset resolver to Meridian's asset hostname and generated manifest:
Marten.configure :production do |config|
config.debug = false
config.host = "0.0.0.0"
config.port = 8000
config.secret_key = ENV.fetch("MARTEN_SECRET_KEY")
config.allowed_hosts = ENV.fetch("MARTEN_ALLOWED_HOSTS")
.split(",")
.map(&.strip)
.reject(&.empty?)
config.assets.url = "https://assets.app.example.com/"
config.assets.manifests = ["src/manifest.json"]
config.templates.cached = true
config.sessions.cookie_secure = true
config.csrf.cookie_secure = true
config.use_x_forwarded_host = true
config.use_x_forwarded_port = true
config.use_x_forwarded_proto = true
endclass HealthHandler < Marten::Handler
def get
respond("ok", content_type: "text/plain")
end
endMarten.routes.draw do
path "/health", HealthHandler, name: "health"
endCommands
podman build -t localhost/my-app:latest .
meridian secret gen MARTEN_SECRET_KEY
meridian setup
meridian plan
meridian check
meridian deployLocal E2E Test
Meridian's repository includes an executable version of this recipe:
make e2e-martenThe test creates a temporary Ubuntu 24.04 Lima VM and exercises the real fresh-host path: bootstrap, secret creation, proxy setup, migration, asset publication, first deploy, and a second blue/green deploy. It also writes a SQLite record through the application and verifies that both the record and the fingerprinted CSS remain available after the color switch.
Requirements are macOS, Lima 2+, Podman, Crystal, curl, expect, ssh-keygen, and nc. KEEP_VM=1 make e2e-marten retains the VM and work directory when debugging.
