Deploy your app
You’ll turn a development server into a production deployment: settings from the environment, a compiled binary in a small container, graceful shutdown, health and metrics endpoints, correct client IPs behind a proxy, and optionally several isolates. Then you’ll deploy it, for example on the gisila panel.
Prerequisites
Section titled “Prerequisites”- A working gisila app.
- Docker, or a host with the Dart SDK for building.
1. Production configuration
Section titled “1. Production configuration”import 'dart:io';
import 'package:gisila/gisila.dart';
Future<void> main() async { final env = Platform.environment; final app = GisilaApp( config: AppConfig( logFormat: LogFormat.json, logRequestBody: false, authenticator: JwtAuthenticator( key: JwtKey.hmac(env['JWT_SECRET']!), algorithms: {'HS256'}, issuer: 'notes-api', ), cors: CorsConfig(allowedOrigins: {env['FRONTEND_ORIGIN']!}, allowCredentials: true), trustedProxies: {'127.0.0.1', '::1', ...?env['TRUSTED_PROXIES']?.split(',')}, rateLimit: const RateLimitConfig(requestsPerMinute: 600), maxRequestBodyBytes: 2 * 1024 * 1024, requestTimeout: const Duration(seconds: 30), healthCheckPath: '/healthz', metrics: MetricsConfig(bearerToken: env['METRICS_TOKEN']), ), );
configureApp(app); // services, singletons, controllers
await app.start( port: int.parse(env['PORT'] ?? '8080'), stopOnSignals: true, drainTimeout: const Duration(seconds: 20), );}Checklist:
- Secrets come from the environment, never from source or annotations.
maxRequestBodyBytesandrequestTimeoutare unlimited by default; set them.trustedProxiesmust include your load balancer when it is not on the same host, or every client shares one rate-limit bucket. Never trust all addresses.- Security headers are on by default. If you serve HTML, set
SecurityHeadersConfig(htmlContentSecurityPolicy: ...). - Compression: leave
compressionoff when the proxy compresses. stopOnSignals: truemakes SIGTERM (from Docker, systemd or the panel) stop accepting connections, drain in-flight requests, runonStophooks, dispose singletons and close the database pool before exiting. KeepdrainTimeoutbelow your platform’s kill timeout.
2. Generate code and compile
Section titled “2. Generate code and compile”Generated .g.dart files must exist at build time: commit them, or run build_runner in the build. Then compile an AOT binary:
dart run build_runner build --delete-conflicting-outputsdart compile exe bin/server.dart -o build/server./build/serverThe binary has no Dart SDK dependency and starts in milliseconds.
3. Container image
Section titled “3. Container image”FROM dart:stable AS buildWORKDIR /appCOPY pubspec.* ./RUN dart pub getCOPY . .RUN dart run build_runner build --delete-conflicting-outputsRUN dart compile exe bin/server.dart -o /app/server
FROM scratchCOPY --from=build /runtime/ /COPY --from=build /app/server /app/serverEXPOSE 8080CMD ["/app/server"]If you serve static files with app.serveStatic, copy that directory into the image too.
4. Health and readiness
Section titled “4. Health and readiness”healthCheckPath answers GET /healthz before auth, metrics and the database. Add a readiness check so an instance that lost its database is taken out of rotation:
AppConfig( healthCheckPath: '/healthz', healthCheck: orm.isHealthy, // GisilaOrmDatabaseProvider from gisila_orm/web.dart)It returns 200 {"status":"ok"} or 503 {"status":"unavailable"}.
5. Metrics
Section titled “5. Metrics”With MetricsConfig, Prometheus scrapes /metrics. Without bearerToken or allow, only direct loopback connections may read it (a reverse proxy on the same host doesn’t count), so a sidecar agent works and the public doesn’t see it.
6. Use every core
Section titled “6. Use every core”A Dart isolate uses one core. To use more, start one app per isolate on a shared port, with a MetricsHub so scrapes see all of them:
Future<void> main() async { final hub = MetricsHub(); for (var i = 1; i < Platform.numberOfProcessors; i++) { await Isolate.spawn(serve, hub.sendPort); } await serve(hub.sendPort);}
Future<void> serve(SendPort hub) async { final app = GisilaApp(config: buildConfig(metrics: MetricsConfig(hub: hub))); configureApp(app); await app.start(port: 8080, shared: true, stopOnSignals: true);}Each isolate has its own memory: in-memory caches and rate-limit buckets are per isolate. Use RedisCacheProvider and RedisRateLimitStore from gisila_redis when limits and cached data must be shared.
7. Deploy on the gisila panel
Section titled “7. Deploy on the gisila panel”The gisila panel is a self-hosted platform for running apps like this one. It builds and runs your app from a git repository, a binary or a ZIP upload, manages environment variables, domains and cron jobs, and shows runtime logs and metrics. Set PORT, DATABASE_URL, JWT_SECRET and the rest as app environment variables; the JSON logs your app writes to stdout show up in the panel’s runtime logs. See the panel documentation for the deployment steps.
Verify it works
Section titled “Verify it works”docker build -t notes-api .docker run --rm -p 8080:8080 -e JWT_SECRET=$(openssl rand -base64 48) \ -e FRONTEND_ORIGIN=https://app.example.com notes-api &curl -s localhost:8080/healthz # {"status":"ok"}docker stop $(docker ps -q --filter ancestor=notes-api)# logs: "gisila: SIGTERM received, shutting down", then the process exits after draining