---
slug: 71-deploy
title: Deploy
description: Desplegá Synsema como un único binario estático — daemon vs systemd, HTTPS automático por flags del CLI, Docker y Kubernetes — con un .syn dev-clean que no se edita para prod.
example_ids: []
---

# Deploy

Synsema viene como un **único binario estático** — sin runtime en el destino. El bloque `serve` queda **dev-clean** en el repo; los knobs de deploy son flags del CLI, así el mismo archivo corre local y en prod **sin ediciones**.

```sh
synsema serve app.syn        # dev: :8080, HTTP plano, sin setup
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto admin@example.com   # prod: HTTPS
```

## Flags de serve

| Flag | Efecto |
|---|---|
| `--port N` | Override de `serve on N` **y** concede `serve(N)`. |
| `--domain d1,d2` | Dominios ACME (SAN). |
| `--tls-auto <email>` | HTTPS automático (ACME) — **es el switch dev↔prod**; necesita un dominio. |
| `--tls-cert / --tls-key` | TLS manual (mutuamente excluyente con `--tls-auto`). |
| `--bind <addr>` | Dirección de bind (default `0.0.0.0`). |
| `--sandbox` / `--cap-set "<lista>"` | Techo del host para todo el server (engine v0.6.7+): handlers, ticks de cron y agentes spawneados sólo pueden `require` dentro de él. `--sandbox` = `stdout,time` + `serve`. |

Precedencia: **flag del CLI > cláusula del archivo > default**. Sin `--tls-auto` → HTTP plano (dev); con `--tls-auto` → TLS (prod).

## Config por entorno (mantené el repo dev-clean)

El archivo `.syn` **no cambia** entre tu laptop y el server, así que `git pull` en prod nunca genera conflictos. Todo lo que difiere vive **fuera** del código.

**Knobs del server → flags del CLI.** `--port`, `--domain`, `--tls-auto` (tabla de arriba). El flag gana sobre la cláusula del archivo.

**Valores de la app → el entorno.** Todo lo que leas con `env("NAME", default)` — p. ej. la URL pública canónica que alimenta tus tags `canonical`/OG/sitemap:

```synsema
require env("SITE_URL")
let SITE be env("SITE_URL", "http://127.0.0.1:8080")   -- default dev; prod lo pisa
```

Seteala en prod desde el entorno (`Environment=SITE_URL=https://example.com` en systemd, `-e SITE_URL=…` en Docker). La resolución es **process env > `.env` > default del código**, así que no hace falta editar el repo.

**Knobs del runtime → el entorno del proceso (no `.env`).** `SYNSEMA_SERVE_WORKERS=N` dimensiona el pool de workers del intérprete que atienden requests (default: uno por core, mín. 2). Subilo para handlers I/O-bound — más requests en vuelo, a costa de más RAM. A diferencia de los valores de la app, este knob configura el **runtime mismo**, así que se lee **una sola vez, del entorno del proceso, antes de que arranque el primer server** — `Environment=` en systemd, `-e` en Docker, `export` en una shell. Ponerlo en `.env` no hace nada: el `.env` sólo alimenta a `env()`/`secret()` dentro de tu programa.

La misma regla cubre todos los knobs del server (engine v0.6.7+): `SYNSEMA_SHUTDOWN_GRACE` (segundos de drain ante SIGINT/SIGTERM, default 10), `SYNSEMA_SSE_KEEPALIVE` (15), `SYNSEMA_WS_SERVER_PING` (30), `SYNSEMA_WS_SUBPROTOCOLS`, `SYNSEMA_WS_MAX_MESSAGE` (16MB), `SYNSEMA_WS_MAX_CONNS` (4096), `SYNSEMA_PROC_MAX` (64), `SYNSEMA_WATCH_MAX` (64, handles `watch` vivos por intérprete, v0.6.9+). `synsema init` los lista comentados en `.env.example` con esa advertencia; la tabla con significados está en [Apps agénticas](/es/0.6.x/47-agentic-apps). `systemctl stop` / `docker stop` mandan SIGTERM → shutdown ordenado (drain, salida 0) — poné `TimeoutStopSec` por encima de la gracia.

**¿Dos apps en un mismo host?** Dale a cada una su puerto — el código queda idéntico: `synsema serve api.syn --port 8081` y `synsema serve admin.syn --port 8082`. Como `--port` pisa `serve on N` **y** concede `serve(N)`, no cambia nada en ninguno de los dos archivos.

## HTTPS, paso a paso (certificado gratis y auto-renovable)

`--tls-auto` obtiene un certificado **gratis** de Let's Encrypt (ACME) y lo **auto-renueva** — sin `certbot`, sin cron. Qué necesitás:

1. Un **dominio** apuntando a la IP del server (un registro DNS `A`/`AAAA`).
2. Puertos **80** y **443** alcanzables — el 80 responde el desafío ACME una vez, después redirige al 443.
3. Correr con el dominio + un email de contacto:

```sh
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto you@example.com
```

Al primer arranque Synsema obtiene el certificado, sirve HTTPS en 443, redirige HTTP→HTTPS, y auto-renueva ~30 días antes del vencimiento (90 días). Los certificados se guardan (`SYNSEMA_CERT_DIR` → `~/.synsema/certs`), así que un restart los **recarga** (sin re-emisión → sin rate-limit).

**El challenge de ACME escucha en el puerto 80** (HTTP-01), y la CA tiene que poder
alcanzarlo desde internet. Si ese puerto ya está tomado —otro servidor, un mapeo de
contenedor, un sidecar— movelo con `SYNSEMA_ACME_HTTP_PORT=8080` y redirigí el `:80`
externo hacia ahí. Como el resto de los knobs de runtime se lee del **entorno del
proceso**, no del `.env`.

**¿Ya tenés un certificado?** Usá `--tls-cert cert.pem --tls-key key.pem` (mutuamente excluyente con `--tls-auto`). Bajo systemd, dale al servicio un `HOME`/`StateDirectory` escribible (abajo) para que pueda guardar los certificados.

## `synsema daemon` vs systemd — elegí uno

- **`synsema daemon start app.syn`** — manager de background integrado (`status`/`logs`/`stop`/`restart`). Sin config de SO, pero **sin arranque al boot ni restart al crashear**. Bueno para dev / máquinas sin systemd.
- **systemd** — supervisor del SO: arranque al boot (`enable`), `Restart=always`, logs en journald. **Usalo para producción.**

```ini
[Service]
ExecStart=/usr/local/bin/synsema serve /opt/app/app.syn --port 443 --domain example.com --tls-auto admin@example.com
Restart=always
StateDirectory=synsema        # HOME escribible para el fallback ~/.synsema/certs
```

## Varios sitios en un mismo host (Synsema es su propio edge proxy)

Dos procesos no pueden bindear ambos `:443`, y **no necesitás nginx/Caddy**. Un proceso Synsema es el **edge**: termina TLS para cada dominio (un cert SAN) y enruta por `Host` a cada backend, que corre en HTTP plano en un puerto privado.

```synsema
-- edge.syn — TLS + routing por Host para cada sitio del server
require serve(443)
require net("127.0.0.1")            -- deny-by-default: el edge sólo habla con localhost

serve on 443
    host "example.com"
        route "GET /"                              -- raíz: /*path NO matchea "/"
            proxy to "http://127.0.0.1:8080"
        route "GET /*path"
            proxy to "http://127.0.0.1:8080"
        route "POST /*path"
            proxy to "http://127.0.0.1:8080"
    host "docs.example.com"
        route "GET /"
            proxy to "http://127.0.0.1:8791"
        route "GET /*path"
            proxy to "http://127.0.0.1:8791"
        route "POST /*path"
            proxy to "http://127.0.0.1:8791"
```

Corré el edge con un cert SAN para todos los dominios; cada backend en HTTP plano, sólo localhost, con su propio repo/versión/servicio:

```sh
synsema serve edge.syn --port 443 --domain example.com,docs.example.com --tls-auto admin@example.com
synsema serve app.syn  --port 8080 --bind 127.0.0.1
synsema serve docs.syn --port 8791 --bind 127.0.0.1
```

- **Gotcha de la raíz:** `route "GET /*path"` exige ≥1 segmento — **no** matchea `/`. Agregá `route "GET /"` también (por método) para que la home llegue al backend.
- **Por método:** `route` liga método+path — declará cada método que reenvíes (GET, POST, …).
- `proxy to` reenvía status + content-type + body **y** los headers end-to-end del upstream (`Location`, `Set-Cookie`, `Cache-Control`, `ETag`, …), así redirects, cookies y caching funcionan a través del edge; los hop-by-hop se descartan.
- **Los streams cruzan el edge.** Una ruta `stream` (SSE) del backend llega evento a evento; una ruta `socket` (WebSocket) del backend se tuneliza después del `101`; las descargas grandes fluyen con su `Content-Length`. Así un backend de chat y una API común pueden ser cada uno su propio proceso — su propio contrato `require`, su puerto, su deploy — detrás de un solo edge TLS. El edge agrega `X-Forwarded-For`/`-Proto`/`-Host`; los targets son sólo `http://` (TLS termina en el edge — un target `https://` falla al arrancar, no por request). Túneles y respuestas sin tamaño cuentan contra `max_streams` y los cierra el shutdown ordenado.
- **Deploys independientes:** reiniciás un backend sin tocar los otros; cada uno puede correr su propia versión de Synsema detrás del mismo edge.

## Un binario con tu programa horneado adentro (`synsema build`)

`synsema serve app.syn` necesita el `.syn` (y sus módulos/templates/assets) presentes en runtime.
`synsema build app.syn -o app` pliega todo eso **dentro del ejecutable** — el engine más tu
programa, sellado con un sha256 — así el entregable es un único archivo que no lleva nada por fuera
(ver [CLI § `synsema build`](/es/0.6.x/70-cli)):

```sh
synsema build app.syn -o app --include data/            # empaqueta assets extra (los mounts estáticos del serve entran solos, v0.6.16+)
synsema build app.syn -o app --cap-set "stdout,net=api.example.com,serve=8080"   # hornea un techo
synsema build app.syn -o app --serve --bind 0.0.0.0 --port 8080   # un binario SERVER (v0.6.16+): runtime de serve + flags de deploy horneados
synsema build desk.syn -o desk --serve --no-console --icon icon.svg --bundle   # una app de ESCRITORIO (v0.6.18+): ver "Tu app en el escritorio"
./app                                                   # corre el programa; el argv es el del programa
```

Como no hay nada que montar, la imagen puede ser `FROM scratch`/distroless:

```dockerfile
FROM scratch
COPY app /app
ENTRYPOINT ["/app"]
```

Los `--cap-set`/`--profile` horneados viajan con el binario y no se pueden subir desde adentro; `app
--engine version` llega al engine pelado; `app --engine update` se rechaza (reconstruí en su lugar).
Para imágenes cross-target, construí contra un binario de engine donante con `--engine-binary
./synsema-linux-x86_64`. Este es el deploy más autocontenido: distribuir `app` es distribuir el
programa entero y sus barandas como un solo artefacto. El mismo build, con `bind "127.0.0.1"`,
`--no-console`, `--icon` y `--bundle`, es una **app de escritorio** — [Tu app en el escritorio](/es/0.6.x/41c-desktop).

## Docker y Kubernetes

La imagen es **solo el binario de Synsema** — el mismo binario estático prebuilt que instala el sitio. El `Dockerfile` del repo lo baja de la release de GitHub y verifica su checksum (no compila); vos montás tu programa `.syn` en `/app`:

```sh
docker build -t synsema .                                   # o --build-arg SYNSEMA_VERSION=v0.4.9
docker run -d --restart unless-stopped -e ANTHROPIC_API_KEY=sk-... \
    -v "$PWD":/app -w /app -p 8080:8080 synsema serve app.syn
```

En K8s, `command: ["synsema", "serve", "/app/app.syn"]` e inyectá claves vía `secretKeyRef`. Los secretos/config vienen del **environment** en prod (override de `.env`) — ver **[Secretos](/es/0.6.x/21-secrets)**.

## Actualizar

Un server corriendo **no** se auto-actualiza. `synsema update` reemplaza el binario en disco; **`systemctl restart` lo aplica**. Los certs TLS persisten (guardados + auto-renovados), así que un restart los recarga — sin pegarle al rate-limit de Let's Encrypt.
