Prezado leitor,
Se você trabalha com dados geoespaciais, principalmente rasters, provavelmente já esbarrou em problemas como:
- Dificuldade de organizar grandes volumes de dados
- Falta de padronização na publicação
- APIs pouco eficientes para busca espacial/temporal
É exatamente aqui que entra o STAC (SpatioTemporal Asset Catalog). Mais do que um formato, o STAC é um padrão moderno para organizar, catalogar e acessar dados geoespaciais, permitindo buscas rápidas e interoperáveis.
Neste guia, você vai aprender a montar um ambiente completo para:
- Organizar seus dados no padrão STAC
- Publicar via API moderna
- Integrar com o GeoServer
- Servir dados raster (COG) de forma eficiente
Este post apresenta, passo a passo, como montar um ambiente completo para criação e publicação de um catálogo STAC (SpatioTemporal Asset Catalog), utilizando Docker, PostGIS, GeoServer e uma API intermediária (adapter). O objetivo é permitir que você organize, publique e consuma dados geoespaciais modernos de forma eficiente.
Antes de começar, é importante entender o papel de cada componente:
- PostGIS → Armazena os metadados espaciais
- pgSTAC → Implementa o padrão STAC dentro do PostgreSQL
- STAC FastAPI → Expõe os dados via API REST
- GeoServer → Publica e renderiza os dados
- Adapter (FastAPI) → Traduz STAC para o formato esperado pelo GeoServer
Um ponto importante: o GeoServer ainda não consome STAC “puro” de forma completa, por isso o uso do adapter é essencial.
1. Atualização do sistema
Antes de instalar qualquer ferramenta, é importante garantir que o sistema esteja atualizado. Isso evita problemas de dependência e incompatibilidade.
> sudo apt update
> sudo apt upgrade -y
2. Instalação do Docker
O Docker será usado para isolar cada componente da arquitetura, garantindo reprodutibilidade. Isso evita conflitos de versão e facilita deploy em outros ambientes.
2.1 Adicionar chave GPG:
> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
> sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
2.2 Adicionar repositório:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
2.3 Instalar Docker + Compose:
> sudo apt update
> sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
Com isso, você terá um ambiente isolado para rodar toda a stack sem conflitos de versão.
3. Estrutura do projeto
Agora vamos organizar os diretórios do projeto:
> sudo mkdir -p /docker/geoserver/plugins
> cd /docker/geoserver/plugins
4. Download dos plugins do GeoServer
O plugins que iremos realizar o download adicionam suporte a:
- COG (Cloud Optimized GeoTIFF) via HTTP/S3
- Integração com STAC
Sem esses plugins, o GeoServer não consegue trabalhar corretamente com dados cloud-native.
> wget https://build.geoserver.org/geoserver/2.27.x/community-latest/geoserver-2.27-SNAPSHOT-cog-http-plugin.zip
> wget https://build.geoserver.org/geoserver/2.27.x/community-latest/geoserver-2.27-SNAPSHOT-cog-s3-plugin.zip
> wget https://build.geoserver.org/geoserver/2.27.x/community-latest/geoserver-2.27-SNAPSHOT-stac-datastore-plugin.zip
5. Dockerfile do GeoServer
Criamos um Dockerfile para incluir os plugins:
cd /docker/geoserver
nano Dockerfile
O conteúdo do arquivo:
FROM docker.osgeo.org/geoserver:2.27.2
COPY plugins/*.jar /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/
Aqui estamos estendendo a imagem padrão do GeoServer para suportar STAC e COG.
6. Arquivo Docker Compose
Agora definimos toda a infraestrutura: Banco de dados (PostGIS), API STAC e GeoServer. Vamos então criar o arquivo docker-compose.yaml:
cd /docker
nano docker-compose.yaml
Esse arquivo é o coração da infraestrutura, ele define como os serviços se comunicam e persistem dados. O conteúdo do arquivo:
volumes:
postgis-data:
geoserver-data:
networks:
internal:
external:
services:
db:
container_name: postgis
image: postgis/postgis:16-3.4
volumes:
- postgis-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=postgis
- POSTGRES_USER=postgis
- POSTGRES_PASSWORD=senha_postgis
- IP_LIST=*
- ALLOW_IP_RANGE=0.0.0.0/0
- POSTGRES_MULTIPLE_EXTENSIONS=postgis,hstore,postgis_topology,postgis_raster,pgrouting,btree_gist
- FORCE_SSL=false
ports:
- "5432:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgis-d postgis"]
interval: 5s
timeout: 5s
retries: 10
networks:
- internal
geoserver:
container_name: geoserver
build: ./geoserver
volumes:
- geoserver-data:/opt/geoserver/data_dir
- /docker/geoserver/imagem_raster:/opt/geoserver/data_dir/coverages
environment:
- TZ=America/Sao_Paulo
- GEOSERVER_ADMIN_USER=admin
- GEOSERVER_ADMIN_PASSWORD=geoserver
- INSTALL_EXTENSIONS=true
- EXTRA_JAVA_OPTS=-Xms4G -Xmx6G
- STABLE_EXTENSIONS=importer,wps,pyramid
- PROXY_BASE_URL=http://192.168.186.140:8083/geoserver
- GEOSERVER_CSRF_WHITELIST=192.168.186.140
- HTTP_SCHEME=http
- CORS_ENABLED=false
ports:
- "8083:8080"
restart: unless-stopped
healthcheck:
test: curl --fail "http://localhost:8080/geoserver/web/wicket/resource/org.geoserver.web.GeoServerBasePage/img/logo.png" || exit 1
interval: 1m30s
timeout: 10s
retries: 3
networks:
- internal
- external
stac:
container_name: stac-api
image: ghcr.io/stac-utils/stac-fastapi-pgstac:latest
environment:
- PGHOST=db
- PGPORT=5432
- PGDATABASE=postgis
- PGUSER=postgis
- PGPASSWORD=senha_postgis
ports:
- "8085:8080"
depends_on:
db:
condition: service_healthy
networks:
- internal
Agora é subir o ambiente:
> docker compose build
> docker compose up -d
7. Criar banco STAC (pgstac)
Instalar a ferramenta:
sudo apt install -y pipx
pipx ensurepath
source ~/.bashrc
pipx install "pypgstac[psycopg]"
Configurar conexão:
export PGHOST=127.0.0.1
export PGPORT=5432
export PGDATABASE=postgis
export PGUSER=postgis
export PGPASSWORD=senha_postgis
Rodar migração:
> pypgstac migrate
Esse comando cria toda a estrutura STAC dentro do banco:
- Tabelas de collections
- Tabelas de items
- Índices espaciais e temporais
Sem isso, a API STAC não consegue funcionar.
8. Criar Collection
Collections funcionam como agrupadores lógicos de dados. Exemplos: Sentinel-2, Ortofotos, Modelos de elevação.
Crie o arquivo:
nano collection.json
Conteúdo do arquivo:
{
"id": "raster-test",
"type": "Collection",
"description": "Teste de raster",
"license": "proprietary",
"extent": {
"spatial": { "bbox": [[-180, -90, 180, 90]] },
"temporal": { "interval": [["2024-01-01T00:00:00Z", null]] }
}
}
Para inserir no banco, execute o comando abaixo:
pypgstac load collections collection.json
9. Criar itens
Os Items representam os dados reais. Exemplo: Um raster específico, um ortomosaico, uma cena de satélite.
Crie o arquivo:
nano item.json
Conteúdo do arquivo:
{
"type": "Feature",
"stac_version": "1.0.0",
"id": "paraiso-ortomosaico",
"collection": "raster-test",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-48.8961561, -25.0593974],
[-48.8764276, -25.0593974],
[-48.8764276, -25.0730781],
[-48.8961561, -25.0730781],
[-48.8961561, -25.0593974]
]]
},
"bbox": [-48.8961561,-25.0730781,-48.8764276,-25.0593974],
"properties": {
"datetime": "2024-01-01T00:00:00Z",
"proj:epsg": 4326
},
"assets": {
"data": {
"href": "http://SEU_IP:9000/rasters/seu_arquivo.tif",
"type": "image/tiff",
"roles": ["data"]
}
}
}
Inserir item no banco:
pypgstac load items item.json
Se você precisar editar o conteúdo do json e realizar um update no banco, use o seguinte comando:
pypgstac load items item.json --method upsert
Você ainda tem uma outra opção que é a criação automático do arquivo json através do rio-stac, para isso você precisa:
pipx install rio-stac --include-deps
rio stac orotomosaico_cog.tif > item.json
Dica importante:
O campo “href”: “http://SEU_IP:9000/rasters/seu_arquivo.tif” do JSON, é o link para o dado real (idealmente um COG acessível via HTTP ou S3).
10. Adapter STAC (compatibilidade com GeoServer)
Essa é uma das partes mais importantes da arquitetura, pois o GeoServer não consume STAC de forma totalmente nativa. Então esse adapter vai resolver as incompatibilidades do GeoServer com STAC, ajustando links e headers.
Para o STAC funcionar perfeitamente no GeoServer, é necessário realizar alguns ajustes de:
- Content-Type
- Href
- Navegação interna
Devido a esse problema, foi desenvolvido um adapter em FastAPI que: intercepta requisições, ajusta os links (href), corrige headers e diferencia chamadas internas e externas.
Criar API:
mkdir /docker/api
cd /docker/api
nano adapter.py
Conteúdo do arquivo adapter.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import requests
import os
app = FastAPI()
STAC_URL = "http://stac-api:8080"
# URLs
PUBLIC_URL = os.getenv("PUBLIC_URL", "http://192.168.186.140:8087")
INTERNAL_URL = "http://stac-adapter:8081"
# -------------------------
# Helper para requisições
# -------------------------
def fetch(url, method="GET", json=None):
if method == "POST":
r = requests.post(url, json=json)
else:
r = requests.get(url)
r.raise_for_status()
return r.json()
# -------------------------
# Detecta se é chamada interna (GeoServer)
# -------------------------
def is_internal(request: Request):
host = request.headers.get("host", "")
return "stac-adapter" in host or "geoserver" in host
# -------------------------
# Fix links (inteligente)
# -------------------------
def fix_links(data, internal=False):
base = INTERNAL_URL if internal else PUBLIC_URL
def fix(obj):
if isinstance(obj, dict):
for k, v in obj.items():
if k == "href" and isinstance(v, str):
obj[k] = v.replace("http://stac-api:8080", base)
else:
fix(v)
elif isinstance(obj, list):
for item in obj:
fix(item)
fix(data)
return data
# -------------------------
# ROOT
# -------------------------
@app.api_route("/", methods=["GET", "HEAD"])
async def root(request: Request):
data = fetch(f"{STAC_URL}/")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/json"
)
# -------------------------
# COLLECTIONS
# -------------------------
@app.api_route("/collections", methods=["GET", "HEAD"])
async def collections(request: Request):
data = fetch(f"{STAC_URL}/collections")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/json"
)
# -------------------------
# COLLECTION
# -------------------------
@app.api_route("/collections/{collection_id}", methods=["GET", "HEAD"])
async def collection(collection_id: str, request: Request):
data = fetch(f"{STAC_URL}/collections/{collection_id}")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/geo+json"
)
# -------------------------
# ITEMS
# -------------------------
@app.api_route("/collections/{collection_id}/items", methods=["GET", "HEAD"])
async def items(collection_id: str, request: Request):
data = fetch(f"{STAC_URL}/collections/{collection_id}/items")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/geo+json"
)
# -------------------------
# ITEM ESPECÍFICO
# -------------------------
@app.api_route("/collections/{collection_id}/items/{item_id}", methods=["GET", "HEAD"])
async def item(collection_id: str, item_id: str, request: Request):
data = fetch(f"{STAC_URL}/collections/{collection_id}/items/{item_id}")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/geo+json"
)
# -------------------------
# SEARCH
# -------------------------
@app.api_route("/search", methods=["GET", "POST", "HEAD"])
async def search(request: Request):
if request.method == "POST":
body = await request.json()
data = fetch(f"{STAC_URL}/search", method="POST", json=body)
else:
data = fetch(f"{STAC_URL}/search")
return JSONResponse(
content=fix_links(data, internal=is_internal(request)),
media_type="application/geo+json"
)
Agora vamos ao conteúdo do arquivo Dockerfile:
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn requests
COPY adapter.py .
CMD ["uvicorn", "adapter:app", "--host", "0.0.0.0", "--port", "8081"]
E pra finalizar, você deve adicionar ao seu docker-compose:
adapter:
container_name: stac-adapter
build: ./api
ports:
- "8087:8081"
depends_on:
- stac
networks:
- internal
- external
Agora é só subir o adapter:
docker compose up -d --build
11. Nginx
Agora, para finalizar, vamos instalar o nginx e deixar tudo rodando externamente na porta 80. O Nginx atua como proxy reverso, centralizando o acesso:
- /geoserver → GeoServer
- /stac → Adapter
- /stac-api → API direta
E ainda ajuda na organização das rotas, facilidade de exposição externa e melhor controle de segurança.
Vamos criar o arquivo nginx-stac.conf:
cd /docker/
nano nginx-stac.conf
Esse arquivo deve conter o seguinte conteúdo:
server {
listen 80;
# -------------------------
# GEOSERVER
# -------------------------
location /geoserver/ {
proxy_pass http://geoserver:8080/geoserver/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# -------------------------
# STAC (via adapter)
# -------------------------
location /stac/ {
proxy_pass http://adapter:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# -------------------------
# STAC DIRETO (opcional)
# -------------------------
location /stac-api/ {
proxy_pass http://stac-api:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Você precisar alterar a seguinte linha do arquivo adapter.py:
PUBLIC_URL = os.getenv("PUBLIC_URL", "http://192.168.186.140:8087")
Para:
PUBLIC_URL = os.getenv("PUBLIC_URL", "http://192.168.186.140/stac")
Agora é só subir o seu container, e pronto:
docker compose up -d --build
Se tudo estiver correto, você verá sua collection retornada via API.
12. Conclusão
Com essa arquitetura, você passa a ter:
- Um catálogo STAC estruturado e escalável
- Uma API moderna para consulta espacial e temporal
- Integração com GeoServer
- Suporte a dados cloud-native (COG)
Mais do que isso, você construiu uma base sólida para aplicações geoespaciais modernas, preparada para lidar com grandes volumes de dados de forma eficiente.