{"id":1644,"date":"2026-09-22T22:42:46","date_gmt":"2026-09-23T01:42:46","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1644"},"modified":"2026-09-22T22:53:21","modified_gmt":"2026-09-23T01:53:21","slug":"vue-go-echo-v5-parte-5-embed-binario-unico-e-docker","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/","title":{"rendered":"Vue.js + Go with Echo v5, part 5: embed, single binary, and Docker"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/vue-go-echo-v5-parte-5.webp\" alt=\"Mascote LinuxPro empacotando um cubo com o logo do Vue numa caixa com o gopher do Go, com a baleia do Docker ao fundo e o cachorro caramelo cyborg brincando\" width=\"1486\" height=\"856\" \/><\/p>\n<p><strong>S\u00e9rie Vue.js + Go com Echo v5:<\/strong> <a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">Parte 1: ambiente e primeira API<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/\">Parte 2: frontend Vue com Vite<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">Parte 3: API REST de tarefas<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-4-login-com-sessao-e-cookie\/\">Parte 4: login com sess\u00e3o e cookie<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/\">Parte 5: embed, bin\u00e1rio \u00fanico e Docker<\/a><\/p>\n<p><strong>C\u00f3digo completo:<\/strong> todos os arquivos da s\u00e9rie est\u00e3o no <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08\">gist vue-go-echo-v5<\/a>, com um README que mostra em qual pasta cada um fica.<\/p>\n<p>Chegamos \u00e0 parte que faz essa dupla valer a pena. At\u00e9 aqui a aplica\u00e7\u00e3o rodava como dois processos: o Vite servindo o Vue e o Go servindo a API. Nesta \u00faltima parte o Vue compilado vai <strong>para dentro do bin\u00e1rio Go<\/strong> com <code>go:embed<\/code>. O resultado \u00e9 <strong>um \u00fanico execut\u00e1vel est\u00e1tico de cerca de 10 MB<\/strong>, que n\u00e3o depende de Node, de pasta de arquivos nem de servidor web para funcionar. Tamb\u00e9m escrevemos um <strong>Dockerfile<\/strong> que compila tudo sem Go ou Node instalados na m\u00e1quina, e publicamos com systemd e Nginx.<\/p>\n<p><!-- more --><\/p>\n<h2>Como o go:embed funciona<\/h2>\n<p>A diretiva <code>\/\/go:embed<\/code> manda o compilador copiar arquivos para dentro do execut\u00e1vel no momento do build. Eles ficam acess\u00edveis como um <code>embed.FS<\/code>, que implementa a interface <code>fs.FS<\/code>, a mesma que o middleware de arquivos est\u00e1ticos do Echo aceita. A documenta\u00e7\u00e3o est\u00e1 em <a href=\"https:\/\/pkg.go.dev\/embed\">pkg.go.dev\/embed<\/a>.<\/p>\n<p>Crie <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-embed-go\"><code>web\/embed.go<\/code><\/a>. Sim, um arquivo Go dentro da pasta do projeto Vue: o <code>go:embed<\/code> s\u00f3 enxerga arquivos no diret\u00f3rio do pacote e abaixo dele, ent\u00e3o o pacote precisa morar ao lado do <code>dist<\/code>:<\/p>\n<pre><code class=\"language-go\">\/\/ Package web entrega o frontend Vue compilado, embutido no bin\u00e1rio.\npackage web\n\nimport (\n\t\"embed\"\n\t\"io\/fs\"\n)\n\n\/\/go:embed all:dist\nvar dist embed.FS\n\n\/\/ Dist devolve o conte\u00fado de web\/dist como raiz do sistema de arquivos.\nfunc Dist() fs.FS {\n\tsub, err := fs.Sub(dist, \"dist\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn sub\n}<\/code><\/pre>\n<ul>\n<li><code>all:dist<\/code> inclui tamb\u00e9m arquivos que come\u00e7am com <code>.<\/code> ou <code>_<\/code>, que o <code>go:embed<\/code> ignora por padr\u00e3o. O Vite pode gerar nomes assim em alguns casos;<\/li>\n<li><code>fs.Sub<\/code> tira o prefixo <code>dist\/<\/code>. Sem ele, o <code>index.html<\/code> estaria em <code>dist\/index.html<\/code> dentro do FS.<\/li>\n<\/ul>\n<h2>Sirva o Vue pelo Echo<\/h2>\n<p>O <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-main-go\"><code>main.go<\/code><\/a> final registra a API e, depois dela, o middleware <code>Static<\/code> apontando para o FS embutido:<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t\"context\"\n\t\"log\/slog\"\n\t\"os\"\n\t\"os\/signal\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com\/exemplo\/vueapp\/internal\/api\"\n\t\"github.com\/exemplo\/vueapp\/web\"\n\t\"github.com\/labstack\/echo\/v5\"\n\t\"github.com\/labstack\/echo\/v5\/middleware\"\n)\n\nfunc main() {\n\tuser := getenv(\"APP_USER\", \"admin\")\n\tpass := os.Getenv(\"APP_PASSWORD\")\n\tif pass == \"\" {\n\t\tslog.Error(\"defina APP_PASSWORD\")\n\t\tos.Exit(1)\n\t}\n\tauth, err := api.NewAuth(user, pass)\n\tif err != nil {\n\t\tslog.Error(\"hash da senha\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n\n\te := echo.New()\n\te.Use(middleware.RequestLogger())\n\te.Use(middleware.Recover())\n\n\tapi.Register(e, auth, api.NewTasks())\n\n\t\/\/ Frontend Vue embutido: arquivos de web\/dist e index.html para as rotas do Vue Router.\n\te.Use(middleware.StaticWithConfig(middleware.StaticConfig{\n\t\tFilesystem: web.Dist(),\n\t\tHTML5:      true,\n\t\t\/\/ \/api\/* nunca cai no index.html: rota inexistente da API devolve 404 em JSON\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn strings.HasPrefix(c.Request().URL.Path, \"\/api\/\")\n\t\t},\n\t}))\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tsc := echo.StartConfig{Address: getenv(\"APP_ADDR\", \"127.0.0.1:8080\")}\n\tif err := sc.Start(ctx, e); err != nil {\n\t\tslog.Error(\"servidor\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc getenv(key, def string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn def\n}<\/code><\/pre>\n<p>Tr\u00eas detalhes fazem a diferen\u00e7a:<\/p>\n<ul>\n<li><strong><code>HTML5: true<\/code><\/strong> resolve o problema das URLs limpas do Vue Router, citado na parte 2. Quem abre <code>\/tarefas<\/code> direto recebe o <code>index.html<\/code>, e o Vue Router assume dali;<\/li>\n<li><strong>o <code>Skipper<\/code> para <code>\/api\/<\/code><\/strong> evita o efeito colateral do modo HTML5. Sem ele, <code>\/api\/rota-errada<\/code> tamb\u00e9m receberia o <code>index.html<\/code> com status 200, e o cliente da API tentaria ler HTML como JSON. No nosso teste, sem o <code>Skipper<\/code> foi exatamente isso que aconteceu; com ele, a resposta \u00e9 <code>{\"message\":\"Not Found\"}<\/code> com 404;<\/li>\n<li><strong><code>echo.StartConfig<\/code> com <code>signal.NotifyContext<\/code><\/strong> d\u00e1 desligamento gracioso. Um <code>SIGTERM<\/code> do systemd ou do Docker espera as requisi\u00e7\u00f5es em andamento terminarem, por at\u00e9 10 segundos por padr\u00e3o, antes de encerrar.<\/li>\n<\/ul>\n<h2>Build do bin\u00e1rio \u00fanico<\/h2>\n<p>A ordem importa: <strong>primeiro o frontend, depois o Go<\/strong>. O <code>go:embed<\/code> precisa encontrar o <code>web\/dist<\/code> na hora de compilar. Num clone novo, em que o <code>dist<\/code> n\u00e3o existe porque est\u00e1 no <code>.gitignore<\/code>, o <code>go build<\/code> falha:<\/p>\n<pre><code class=\"language-text\">web\/embed.go:9:12: pattern all:dist: no matching files found<\/code><\/pre>\n<p>Para n\u00e3o depender de mem\u00f3ria, deixe a ordem num <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-makefile\"><code>Makefile<\/code><\/a> na raiz. Lembre que as linhas de comando come\u00e7am com <strong>TAB<\/strong>:<\/p>\n<pre><code class=\"language-makefile\">build:\n\tcd web &amp;&amp; npm ci &amp;&amp; npm run build\n\tCGO_ENABLED=0 go build -trimpath -ldflags \"-s -w\" -o bin\/vueapp .\n\ndev-api:\n\tAPP_PASSWORD=dev go run .\n\ndev-web:\n\tcd web &amp;&amp; npm run dev\n\nclean:\n\trm -rf bin web\/dist<\/code><\/pre>\n<pre><code class=\"language-bash\">make build\nls -lh bin\/vueapp\nfile bin\/vueapp<\/code><\/pre>\n<pre><code class=\"language-text\">-rwxr-xr-x 1 user user 9,5M bin\/vueapp\nbin\/vueapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ... stripped<\/code><\/pre>\n<ul>\n<li><code>CGO_ENABLED=0<\/code> gera um bin\u00e1rio est\u00e1tico, que roda em qualquer Linux da mesma arquitetura sem depender da glibc;<\/li>\n<li><code>-ldflags \"-s -w\"<\/code> remove s\u00edmbolos de depura\u00e7\u00e3o e reduz o tamanho;<\/li>\n<li><code>-trimpath<\/code> tira os caminhos da sua m\u00e1quina de dentro do execut\u00e1vel.<\/li>\n<\/ul>\n<p>Para provar que n\u00e3o h\u00e1 depend\u00eancia de arquivos externos, copie o bin\u00e1rio para outra pasta e rode:<\/p>\n<pre><code class=\"language-bash\">cp bin\/vueapp \/tmp\/ &amp;&amp; cd \/tmp\nAPP_PASSWORD=segredo123 .\/vueapp<\/code><\/pre>\n<p>Abra <code>http:\/\/127.0.0.1:8080<\/code>: login, tarefas e navega\u00e7\u00e3o funcionando, com uma porta e um processo s\u00f3. Para outra arquitetura, basta <code>GOARCH=arm64<\/code> no <code>go build<\/code>, porque o frontend compilado \u00e9 o mesmo.<\/p>\n<h2>Build com Docker, sem instalar Go nem Node<\/h2>\n<p>Um <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-dockerfile\"><code>Dockerfile<\/code><\/a> <a href=\"https:\/\/docs.docker.com\/build\/building\/multi-stage\/\">multi-stage<\/a> resolve o build em qualquer m\u00e1quina que tenha Docker, com vers\u00f5es fixas de Node e Go, e ainda gera uma imagem m\u00ednima. Crie o <code>Dockerfile<\/code> na raiz:<\/p>\n<pre><code class=\"language-dockerfile\"># 1) Frontend: compila o Vue com Node\nFROM node:24-alpine AS web\nWORKDIR \/src\/web\nCOPY web\/package.json web\/package-lock.json .\/\nRUN npm ci\nCOPY web\/ .\/\nRUN npm run build\n\n# 2) Backend: compila o Go j\u00e1 com o web\/dist embutido\nFROM golang:1.27.1-alpine AS build\nWORKDIR \/src\nCOPY go.mod go.sum .\/\nRUN go mod download\nCOPY . .\nCOPY --from=web \/src\/web\/dist .\/web\/dist\nRUN CGO_ENABLED=0 go build -trimpath -ldflags \"-s -w\" -o \/out\/vueapp .\n\n# 3) S\u00f3 o bin\u00e1rio, para extrair com --output\nFROM scratch AS bin\nCOPY --from=build \/out\/vueapp \/vueapp\n\n# 4) Imagem final: nada al\u00e9m do bin\u00e1rio, rodando sem root\nFROM scratch\nCOPY --from=build \/out\/vueapp \/vueapp\nUSER 65534:65534\nENV APP_ADDR=0.0.0.0:8080\nEXPOSE 8080\nENTRYPOINT [\"\/vueapp\"]<\/code><\/pre>\n<p>E um <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-dockerignore\"><code>.dockerignore<\/code><\/a>, para n\u00e3o mandar ao build o <code>node_modules<\/code> e artefatos locais:<\/p>\n<pre><code class=\"language-text\">.git\nbin\nweb\/node_modules\nweb\/dist<\/code><\/pre>\n<p>Os est\u00e1gios ajudam o cache: <code>package.json<\/code> e <code>go.mod<\/code> s\u00e3o copiados antes do c\u00f3digo, ent\u00e3o as depend\u00eancias s\u00f3 s\u00e3o baixadas de novo quando mudam.<\/p>\n<h3>Op\u00e7\u00e3o 1: s\u00f3 o bin\u00e1rio<\/h3>\n<p>O est\u00e1gio <code>bin<\/code> existe para uma coisa: extrair o execut\u00e1vel para a sua m\u00e1quina com o <a href=\"https:\/\/docs.docker.com\/build\/exporters\/local-tar\/\">exportador local<\/a> do BuildKit:<\/p>\n<pre><code class=\"language-bash\">docker build --target bin --output type=local,dest=bin .\nls -lh bin\/vueapp<\/code><\/pre>\n<p>Voc\u00ea recebe o mesmo bin\u00e1rio est\u00e1tico do <code>make build<\/code>, sem ter Go ou Node instalados. \u00c9 a forma mais simples de compilar numa m\u00e1quina limpa ou num pipeline de CI.<\/p>\n<h3>Op\u00e7\u00e3o 2: a imagem<\/h3>\n<pre><code class=\"language-bash\">docker build -t vueapp .\ndocker images vueapp\ndocker run -d --name vueapp -p 127.0.0.1:8080:8080 -e APP_PASSWORD=segredo123 vueapp<\/code><\/pre>\n<pre><code class=\"language-text\">REPOSITORY   TAG       SIZE\nvueapp       latest    9.9MB<\/code><\/pre>\n<p>A imagem parte do <code>scratch<\/code>, vazio. Ela cont\u00e9m <strong>s\u00f3 o bin\u00e1rio<\/strong>: sem shell, sem gerenciador de pacotes, sem nada para um invasor aproveitar. Roda como o usu\u00e1rio <code>65534<\/code> (nobody), e n\u00e3o como root. Dentro do cont\u00eainer, o <code>APP_ADDR<\/code> \u00e9 <code>0.0.0.0:8080<\/code>, porque <code>127.0.0.1<\/code> ali dentro n\u00e3o seria alcan\u00e7\u00e1vel pelo mapeamento de porta; do lado do host, o <code>-p 127.0.0.1:8080:8080<\/code> continua expondo s\u00f3 localmente.<\/p>\n<h3>Op\u00e7\u00e3o 3: Docker Compose<\/h3>\n<p>Para subir com um comando, crie <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-compose-yaml\"><code>compose.yaml<\/code><\/a>:<\/p>\n<pre><code class=\"language-yaml\">services:\n  app:\n    build: .\n    image: vueapp:latest\n    restart: unless-stopped\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    environment:\n      APP_USER: admin\n      APP_PASSWORD: ${APP_PASSWORD:?defina APP_PASSWORD}<\/code><\/pre>\n<pre><code class=\"language-bash\">APP_PASSWORD='troque-esta-senha' docker compose up -d --build<\/code><\/pre>\n<p>A sintaxe <code>${APP_PASSWORD:?...}<\/code> faz o Compose recusar a subida se a senha n\u00e3o estiver definida, em vez de iniciar com um valor vazio:<\/p>\n<pre><code class=\"language-text\">required variable APP_PASSWORD is missing a value: defina APP_PASSWORD<\/code><\/pre>\n<p>Para aprender o b\u00e1sico de cont\u00eaineres, veja o nosso <a href=\"\/2017\/08\/curso-de-docker-gratis\/\">curso de Docker<\/a>.<\/p>\n<h2>Sem Docker: systemd<\/h2>\n<p>Se preferir o bin\u00e1rio direto no servidor, um servi\u00e7o systemd com usu\u00e1rio pr\u00f3prio e sistema de arquivos somente leitura resolve. Veja tamb\u00e9m o guia de <a href=\"\/2026\/09\/dominando-o-systemd-comandos-essenciais\/\">comandos essenciais do systemd<\/a>.<\/p>\n<pre><code class=\"language-bash\">sudo install -m 0755 bin\/vueapp \/usr\/local\/bin\/vueapp\nsudo install -d -m 0750 \/etc\/vueapp\necho 'APP_PASSWORD=troque-esta-senha' | sudo tee \/etc\/vueapp\/env &gt;\/dev\/null\nsudo chmod 0600 \/etc\/vueapp\/env<\/code><\/pre>\n<p><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-vueapp-service\"><code>\/etc\/systemd\/system\/vueapp.service<\/code><\/a>:<\/p>\n<pre><code class=\"language-ini\">[Unit]\nDescription=Vue + Go (Echo v5)\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nEnvironmentFile=\/etc\/vueapp\/env\nEnvironment=APP_ADDR=127.0.0.1:8080\nExecStart=\/usr\/local\/bin\/vueapp\nDynamicUser=yes\nNoNewPrivileges=yes\nProtectSystem=strict\nProtectHome=yes\nPrivateTmp=yes\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n<pre><code class=\"language-bash\">sudo systemctl daemon-reload\nsudo systemctl enable --now vueapp\ncurl -s http:\/\/127.0.0.1:8080\/api\/health\njournalctl -u vueapp -f<\/code><\/pre>\n<p>O <code>DynamicUser=yes<\/code> cria um usu\u00e1rio tempor\u00e1rio para o servi\u00e7o, e o <code>EnvironmentFile<\/code> com permiss\u00e3o <code>0600<\/code> mant\u00e9m a senha fora da unit e do <code>ps<\/code>. O <code>SIGTERM<\/code> do <code>systemctl stop<\/code> aciona o desligamento gracioso.<\/p>\n<h2>HTTPS com Nginx na frente<\/h2>\n<p>O cookie de sess\u00e3o n\u00e3o pode trafegar em HTTP fora do <code>localhost<\/code>. Coloque o Nginx na frente, com o certificado, e repasse <code>X-Forwarded-Proto<\/code>: \u00e9 esse cabe\u00e7alho que liga o <code>Secure<\/code> do cookie, como vimos na parte 4.<\/p>\n<pre><code class=\"language-nginx\">server {\n    listen 443 ssl;\n    server_name app.exemplo.com;\n\n    ssl_certificate     \/etc\/letsencrypt\/live\/app.exemplo.com\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/app.exemplo.com\/privkey.pem;\n\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:8080;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}<\/code><\/pre>\n<p>S\u00f3 aceite <code>X-Forwarded-Proto<\/code> quando o Go estiver escutando em <code>127.0.0.1<\/code>, atr\u00e1s do proxy. Se a porta estivesse exposta, qualquer cliente poderia enviar esse cabe\u00e7alho.<\/p>\n<h2>Arquivos desta parte no gist<\/h2>\n<p>Cada arquivo abre direto no <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08\">gist da s\u00e9rie<\/a>. O gist traz a <strong>vers\u00e3o final<\/strong> do projeto: <code>main.go<\/code> e <code>routes.go<\/code> ainda ganham login na parte 4 e o frontend embutido na parte 5.<\/p>\n<ul>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-embed-go\"><code>embed.go<\/code><\/a> (em <code>web\/embed.go<\/code>)<\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-main-go\"><code>main.go<\/code><\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-makefile\"><code>Makefile<\/code><\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-dockerfile\"><code>Dockerfile<\/code><\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-dockerignore\"><code>.dockerignore<\/code><\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-compose-yaml\"><code>compose.yaml<\/code><\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-vueapp-service\"><code>vueapp.service<\/code><\/a> (em <code>\/etc\/systemd\/system\/vueapp.service<\/code>)<\/li>\n<\/ul>\n<h2>Recapitulando a s\u00e9rie<\/h2>\n<ul>\n<li><a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">Parte 1<\/a>: ambiente, Echo v5 e a primeira rota, com o <code>*echo.Context<\/code> da v5;<\/li>\n<li><a href=\"\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/\">Parte 2<\/a>: Vue 3 com Vite, Vue Router e o proxy de desenvolvimento;<\/li>\n<li><a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">Parte 3<\/a>: API REST com grupos, <code>Bind<\/code>, valida\u00e7\u00e3o e <code>HTTPError<\/code>;<\/li>\n<li><a href=\"\/2026\/09\/vue-go-echo-v5-parte-4-login-com-sessao-e-cookie\/\">Parte 4<\/a>: login com bcrypt, cookie <code>HttpOnly<\/code> e middleware de autentica\u00e7\u00e3o;<\/li>\n<li>Parte 5: <code>go:embed<\/code>, bin\u00e1rio \u00fanico de cerca de 10 MB, Docker, systemd e Nginx.<\/li>\n<\/ul>\n<p>Daqui, os pr\u00f3ximos passos naturais s\u00e3o trocar o armazenamento em mem\u00f3ria por SQLite ou PostgreSQL, guardar as sess\u00f5es no banco e adicionar testes com o pacote <code>echotest<\/code> do pr\u00f3prio Echo. A s\u00e9rie <a href=\"\/2026\/09\/phpvirtualbox-em-go-parte-1-echo-vue-e-binario-unico\/\">phpVirtualBox em Go<\/a> mostra essa mesma arquitetura aplicada a um projeto real.<\/p>\n<p><strong>S\u00e9rie Vue.js + Go com Echo v5:<\/strong> <a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">Parte 1: ambiente e primeira API<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/\">Parte 2: frontend Vue com Vite<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">Parte 3: API REST de tarefas<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-4-login-com-sessao-e-cookie\/\">Parte 4: login com sess\u00e3o e cookie<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/\">Parte 5: embed, bin\u00e1rio \u00fanico e Docker<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Embed Vue into the Go binary with go:embed, generate a single executable, build with multi-stage Docker, and deploy with systemd and Nginx.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[45,2],"tags":[424,156,58,31,123,468],"class_list":["post-1644","post","type-post","status-publish","format-standard","hentry","category-desenv","category-linux","tag-api","tag-docker","tag-go","tag-golang","tag-systemd","tag-vue"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1644","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1644"}],"version-history":[{"count":2,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1644\/revisions"}],"predecessor-version":[{"id":1659,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1644\/revisions\/1659"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1644"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1644"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1644"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}