<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://madeindra.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://madeindra.dev/" rel="alternate" type="text/html" /><updated>2026-04-05T09:03:19+00:00</updated><id>https://madeindra.dev/feed.xml</id><title type="html">madeindra.dev</title><subtitle>A blog by Made Indra sharing insights, tutorials, and stories about software development, technology trends, personal passions, plus a bit of everything else!</subtitle><author><name>Made Indra</name></author><entry><title type="html">Hidden Ways to Pay Less</title><link href="https://madeindra.dev/hidden-ways-to-pay-less/" rel="alternate" type="text/html" title="Hidden Ways to Pay Less" /><published>2026-04-05T00:00:00+00:00</published><updated>2026-04-05T00:00:00+00:00</updated><id>https://madeindra.dev/hidden-ways-to-pay-less</id><content type="html" xml:base="https://madeindra.dev/hidden-ways-to-pay-less/"><![CDATA[<p>Let me tell you something, paying full is optional.</p>

<p>Well not always, but most of the times there are some tricks you should try.</p>

<hr />

<h2 id="1-student-or-corporate-discounts">1. Student or Corporate Discounts</h2>

<p>A log of service offers a significant discount for students or employee of specific companies. For example Github, Spotify, Adobe, etc.</p>

<p>For students, they usually asked you to sign up using university issue email or uploading you student ID as a proof. Always check whether the service you are using offer special pricing for students.</p>

<p>As for corporate discounts, you should check your HR team, sometimes they may have negotiated deals you didn’t know existed.</p>

<hr />

<h2 id="2-wait-for-a-sale-event">2. Wait for a Sale Event</h2>

<p>This one is a no-brainer. If you plan to buy something and there is a special event coming in the corner and your purchase is not urgent, you should wait.</p>

<p>Not only for digital services, offline retailers often give discounts too for events like Black Friday, Cyber Monday, end-of-season, anniversary, or holiday. This is something that you can predict just by checking the calendar.</p>

<hr />

<h2 id="3-buy-in-a-group-bulk-purchase">3. Buy in a Group (Bulk Purchase)</h2>

<p>Some platform have a bulk pricing where the price per unit could help you save some money.</p>

<p>This works especially well for books, software, or license. One person could coordinate the purchase and split the cost among the group.</p>

<hr />

<h2 id="4-bundled-purchases">4. Bundled Purchases</h2>

<p>Apps and services sometimes sell bundles: a package of multiple products at a price lower than if you bought each separately. App stores, game platforms, and productivity suites do this regularly.</p>

<p><a href="https://setapp.com/">Setapp</a> is a good example of this.</p>

<p>Before buying anything individually, check whether a bundle exists. You might pay a little more upfront but end up with tools you’ll actually use, at a fraction of the individual cost.</p>

<hr />

<h2 id="5-search-for-coupons-before-checkout">5. Search for Coupons Before Checkout</h2>

<p>This one sounds obvious, but a lot of people skip it. Before hitting “Pay Now”, open a new tab and search: <code class="language-plaintext highlighter-rouge">[store name] coupon code [month] [year]</code>.</p>

<p>Sites like Honey (browser extension), RetailMeNot, and CouponFollow aggregate active promo codes. There’s no reason not to try, it takes ten seconds and sometimes saves 10-20%.</p>

<hr />

<h2 id="6-refurbished-or-open-box-items">6. Refurbished or Open-Box Items</h2>

<p>Buying refurbished doesn’t mean buying broken. Most certified refurbished products from manufacturers like Apple or Playstation go through the same quality checks as new ones, often come with a warranty, and are priced noticeably lower.</p>

<p>Open-box items are similar, these are returned products that are functionally new but can no longer be sold as such.</p>

<p>Amazon has a specific section called Amazon Renewed for refurbished, returned, and open-box product.</p>

<hr />

<h2 id="7-subscribe-to-the-newsletter">7. Subscribe to the Newsletter</h2>

<p>This one feels counterintuitive, but many websites send a first-time subscriber discount, usually 10-15% off your first purchase. So if you don’t like more emails, just sign up, use the code, then unsubscribe.</p>

<p>Beyond the welcome discount, newsletter subscribers often get early access to sales and exclusive promo codes that aren’t advertised publicly. If you buy from a store regularly, being on the list is worth it.</p>

<hr />

<h2 id="8-purchasing-power-parity">8. Purchasing Power Parity</h2>

<p>Many digital services price their products differently depending on the country the buyer is in. A subscription that costs $15/month in the US might cost $4/month in Turkey or India. Using a VPN to appear in a lower-cost region can unlock that pricing.</p>

<p>One thing to remember, this trick is technically something you shouldn’t do. Sometimes the service could be smart enough to detect the difference between the indicated country and the the payment method (e.g. credit card issuer country, currency of the card).</p>

<p>Let’s just say I put it here for education purpose.</p>

<hr />

<h2 id="9-leave-it-in-your-cart">9. Leave It in Your Cart</h2>

<p>Add the item to your cart and walk away. Don’t checkout. Wait a day or two.</p>

<p>A surprisingly large number of e-commerce sites track abandoned carts and will email a discount code to bring you back. Not every store does this, but enough do that it’s worth trying, especially for bigger purchases.</p>

<p>The worst case is nothing happens.</p>

<hr />

<h2 id="bonus-price-tracking-tools">Bonus: Price Tracking Tools</h2>

<p>If have a long list of wishlist and you’re not in a hurry, use a price tracker to monitor when an item hits its lowest point.</p>

<p>Some tool can track prices over time. So, instead of guessing whether today’s price is a good deal, you’ll have the historical data to know for sure. They could also send alerts when the price drop below the value you set</p>

<p>This tricks apply to anything: <a href="https://steamdb.info/">SteamDB</a> for Steam games, <a href="https://psdeals.net/">PS Deals</a> for Playstation games, <a href="https://www.google.com/travel/flights">Google Flight</a> for flight tickets, and <a href="https://www.idealo.de/">idealo</a> for a lot of products in Germany.</p>

<p>Other that I haven’t try but worth to mentions are <a href="https://camelcamelcamel.com/">CamelCamelCamel</a> for products in Amazon, <a href="https://shopping.google.com/">Google Shopping</a>, and browser extensions like Keepa.</p>

<hr />

<p>All the tricks above probably will add some extra minutes to your to-do list before purchasing. But isn’t it better to spend some time to save some money so you can buy more things?</p>

<p>I will be sure to add another tricks to the list here later if I found some more (｡•̀ᴗ-)✧</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@tamanna_rumee?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Tamanna Rumee on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Life" /><summary type="html"><![CDATA[Want a hidden discount? There's actually a lot of way not to pay a full price on the internet!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2026-04-05-hidden-ways-to-pay-less.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2026-04-05-hidden-ways-to-pay-less.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How’s Life So Far in Germany</title><link href="https://madeindra.dev/hows-life-so-far-in-germany/" rel="alternate" type="text/html" title="How’s Life So Far in Germany" /><published>2026-01-05T00:00:00+00:00</published><updated>2026-01-05T00:00:00+00:00</updated><id>https://madeindra.dev/hows-life-so-far-in-germany</id><content type="html" xml:base="https://madeindra.dev/hows-life-so-far-in-germany/"><![CDATA[<p>As I write this, the temperature outside has dropped below 0. The snow has stopped falling, but the pavements around here are either covered in snow or wet and slippery. Not to mention that the area I lived in was affected by the power outage. Can you imagine what it’s like to live through winter with no power, no heating, and no internet access? I sat in the dark, wrapped in every blanket I owned, watching my breath form small clouds in the cold air. That was how I started my first weekend of 2026.</p>

<p>No, this isn’t a post about how much I hate living here. In fact, I love living in Germany.</p>

<p>Whenever I introduce myself, numerous people ask “Why would you move here from a warm tropical island? You were living a dream”. Well, now you will also know how I answer them.</p>

<p>Let me take you back to where it all began.</p>

<hr />

<p>I spent the start of 2025 buried in paperwork - employment contract, apostille, translations. Two months of gathering documents, one more month of waiting. Then the email arrived. I was going to Germany.</p>

<p>Berlin was the first city I visited in Germany. To be perfectly honest, it’s even my first time being in Europe.</p>

<p>When I stepped outside of the Berlin-Brandenburg Airport, I stopped. A cool 15-degree breeze touched my face. I took a deep breath - and for the first time in years, my lungs didn’t protest. No exhaust. No smog. Just air. I was ready for the so-called “German directness”, but the taxi driver who picked me up was very friendly. We chatted briefly about the Spring weather. “Guten Tag” was the only German word that I used, the rest was all in English. I knew it was silly of me to come unprepared.</p>

<p><img src="/assets/images/2026-01-05-hows-life-so-far-in-germany-taxi.webp" alt="The taxi was a luxurious one" /></p>

<p>For months, I lived in a shared apartment with people of several different countries. One day, while browsing for places to visit, I stumbled upon Berlin’s official website - a section about “Sakura in Berlin”. The photos showed the exact streets I walked daily, transformed into tunnels of pink petals. I had missed it by weeks.</p>

<p>My first impression of Berlin is that the weather is unpredictable. There’s a saying that goes “There is no bad weather, only bad clothing” and the people here live by it.</p>

<p>During the spring I was occasionally shaking outside because of the cold, but perhaps that was just me getting used to the breezy weather. Nevertheless, my point still stands because during the summer there would be two weeks of warm sunshine followed by a week of wet and cold rain. I even got to experience a warm winter in Berlin.</p>

<p>It was unusual, but again, you just have to be in the proper clothing.</p>

<hr />

<p>Adapting to the weather was one thing. But getting around? That was surprisingly easy.</p>

<p>One thing that I love the most about Berlin is definitely the public transportation.</p>

<p>Yes, it’s not the most punctual one. But, I can survive here without having my own vehicle. I can get anywhere with just my monthly ticket.</p>

<p>There was a time when I missed my regional trains because they were either cancelled or delayed so badly. I ended up taking a connecting bus instead - but I arrived at noon, and my connection wasn’t until 4. Four hours of watching strangers come and go, none of them waiting for the same ride as me. But it’s a story for another day.</p>

<p>When I came back to Indonesia for a month, this was the thing that I missed the most. I hate driving because the traffic is always unpredictable. Using a ride-hailing service is also a gamble, sometimes I end up with a driver who drives as if they are above the law.</p>

<p>Here, I can hop on to a bus, change to a train or to a tram, and get to my destination not feeling tired nor anxious about getting bumped by other drivers. Peace of mind is number one.</p>

<hr />

<p>And because getting around is so easy, have I told you that it’s very nice to be in the centre of Europe?</p>

<p>It was not long until I found myself in Tallinn and Helsinki. One hour flight from Berlin, then two hours by ferry. Cobblestone streets, medieval towers, the smell of the Baltic Sea.</p>

<p><img src="/assets/images/2026-01-05-hows-life-so-far-in-germany-old-town.webp" alt="Old town was very beautiful" /></p>

<p>The cities in Germany also have their own charm. Every time I visited a new place, I would bring home memorable photos. Classic architecture, bronze statues, pieces of art. Each city had something worth capturing.</p>

<p>That’s not all, in Berlin alone there are hundreds of open spaces. You can find parks everywhere you go. I often saw people walking their cute dogs, parents playing with their children or socialising with the other parents, and musicians singing songs they themselves composed.</p>

<p>Summer was the best season where parks would never be empty. The smell of grilled sausages would drift across the grass. Laughter. Clinking bottles. Someone always had a speaker playing music, sometimes a bit too loud, but nobody seemed to mind.</p>

<hr />

<p>Those park scenes bring me to another surprise. I was told that the people here would not be friendly, like, grumpy all the time. It was not the case. In my opinion, the elders are the most open, they would not hesitate to throw back smiles, especially to children.</p>

<p>Indonesia is known for being friendly, but we don’t have the custom of cashiers greeting customers and saying farewell. I love hearing “Hallo!” when I’m going to the cashier and the usual “Tschüss/Ciao/Schönen Tag noch” to end the interactions.</p>

<p>For a country which is said to not tolerate bullshit, I say, a lot of Germans can be quite friendly.</p>

<p>Work has been just as welcoming. I can’t say much because the company I work for has an international environment. The culture is amazing and I don’t have any issue. People are eager to give their best and I can talk to anyone without being bothered by hierarchy. I apologise if I sound like I’m showing off, but that’s just the reality.</p>

<p>There’s a lot to learn at work due to the differing business needs, but I’m grateful for the opportunity.</p>

<hr />

<p>If you have read this far, you must’ve already known that I love living the life here.</p>

<p>Hey, there are also times when I get annoyed. Having to do everything by letter. Waiting months for an appointment with a general practitioner. Queuing for hours with more than 25 people to compete for a 1-bedroom apartment. Walking to a food stall just to be greeted with “Kartenzahlung erst ab 10 Euro”, card payments only above 10 euros, and having to carry banknotes everywhere.</p>

<p>I accept those complexities as my new normal.</p>

<p>Even when someone was mean because I didn’t understand what they said and they didn’t understand my broken German words, I didn’t regret coming here. At the end of the day, I would grab a Döner mit Pommes frites, take it home, and eat it alone in my room. Sometimes with a few tears. Sometimes with a drink or two. Sometimes I wouldn’t remember falling asleep.</p>

<p>I’m not joking, German is super hard to master. Despite learning it for the last 6 months straight, two hours every weekday, I still struggle to converse spontaneously. Reading the news is also a challenge I have yet to tackle.</p>

<p>So here I am, back in my apartment, a warm mug of milk in my hands, the heating finally restored. The snow outside doesn’t seem so bad anymore. If anything, it reminds me why I stayed.</p>

<p><img src="/assets/images/2026-01-05-hows-life-so-far-in-germany-trad.webp" alt="Wearing the traditional clothing to blend in" /></p>]]></content><author><name>Made Indra</name></author><category term="Life" /><summary type="html"><![CDATA[A personal reflection on moving from Indonesia to Germany: the good, the hard, and why I stayed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2026-01-05-hows-life-so-far-in-germany.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2026-01-05-hows-life-so-far-in-germany.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Self-Doubt: Is It an Unwinnable Battle</title><link href="https://madeindra.dev/self-doubt-is-it-an-unwinnable-battle/" rel="alternate" type="text/html" title="Self-Doubt: Is It an Unwinnable Battle" /><published>2025-04-29T00:00:00+00:00</published><updated>2025-04-29T00:00:00+00:00</updated><id>https://madeindra.dev/self-doubt-is-it-an-unwinnable-battle</id><content type="html" xml:base="https://madeindra.dev/self-doubt-is-it-an-unwinnable-battle/"><![CDATA[<p>Let me confess something.</p>

<p>There’s something wrong with me—or at least, that’s what I often believe.</p>

<p>For as long as I can remember, I’ve been at war with self-doubt. It’s exhausting to give your all while constantly whispering to yourself that you’ll never be enough.</p>

<p>Looking back, it almost feels like life followed some invisible straight line, neatly connecting the dots to where I am now. But truth is, that path was anything but straight. I wandered. I hesitated. I got lost.</p>

<p>I knew where I wanted to go. But somehow, I couldn’t walk toward it. No guide, no map—just an endless loop of second-guessing myself, afraid of what failure might do to me.</p>

<hr />

<p>When I was a kid, I loved everything about technology. I dreamed of creating things myself. We had a computer at home, but no one around me could teach me how to use it beyond the basics. We didn’t even have internet until I was 14.</p>

<p>By then, I had already drifted. Surrounded by smart people, I felt I didn’t belong in that world. I gave up on the dream. I just wanted to enjoy what others made, not build anything of my own.</p>

<p>I don’t remember when exactly school stopped making sense to me. I barely passed anything. The only class I did well in was English, which made me think about choosing a language-related major. My parents didn’t approve. I even considered Psychology, something more human, but they shut that down too.</p>

<p>Eventually, we compromised. They pushed for engineering. I didn’t care anymore, so I picked a random major in a less-known university—just to get it over with.</p>

<p>It was miserable.</p>

<p>For two years, I kept failing classes—especially those filled with math and physics. I thought I was doomed. But later, when programming classes came in, things changed. I wasn’t just passing—I was doing well. More than that, I enjoyed it. I even started helping friends who were struggling, and surprisingly, they said I was good at explaining things.</p>

<p>That made something click.</p>

<p>Maybe I could teach. Maybe that could be my future.</p>

<p>But to do that, I’d need a better degree.</p>

<hr />

<p>That’s when my self-doubt came back stronger than ever.</p>

<p>I wanted to study abroad. I believed being around cutting-edge tech would shape me. But I doubted I could get a scholarship. Private universities were out of the question—too expensive. So, I settled for a local public uni. People said I was lucky to get in, and I think they were right. It was a new major. Maybe they just needed more students. I passed the basic entrance tests, but even then, I didn’t feel like I earned my spot.</p>

<p>When I saw the curriculum, my heart sank. So many algorithm-heavy courses—so much math. I was terrified.</p>

<p>I told myself: If I fail this, I have nothing left.</p>

<p>So I did something I’d never done before: I bought an advanced math textbook and started learning ahead.</p>

<p>It was hell.</p>

<p>But it worked.</p>

<p>Despite every voice in my head telling me I’d fail again, I aced the class. I got an A in advanced math.</p>

<p>Me. The guy who thought he could never do it.</p>

<p>That one win changed something in me. I started to believe, just a little.</p>

<hr />

<p>But self-doubt never really left. It still lingers in the background—sometimes loud, sometimes quiet. But always there.</p>

<p>I remember admiring someone who worked at a state-owned company, thinking, He’s a real man. A provider. I’ll never be like that.</p>

<p>Yet somehow, I got into a similar company’s management trainee program on my first try—even after telling my parents not to get their hopes up.</p>

<p>Later, I got offers from tech companies. I didn’t think I could survive outside of the structure and safety of a corporation. So I said no. I stayed.</p>

<p>I’m a family man. That’s both a blessing and a weight. Eventually, I knew I had to try again—for them, and for me. I gave it my all, and finally, I walked away from my comfort zone.</p>

<hr />

<p>My next job was with an offshore team, working with foreigners. I assumed I’d be invisible—just another cog in the system. I told myself: You won’t make friends. You’ll just get the job done.</p>

<p>Then the VP visited.</p>

<p>I wasn’t excited. I just wanted to meet other offshore folks. But when I heard the VP speak, something shifted. He believed in the company. He had vision. It sparked something in me.</p>

<p>The next year, I met the CTO. He wasn’t just a leader—he was kind. He saw me as a person, not just an employee.</p>

<p>I gave everything I had to that company. And slowly, every doubt I had at the start… disappeared.</p>

<p>When I left the company, I didn’t feel like I was running away. I felt proud. Fulfilled.</p>

<p>My teammates? They weren’t just coworkers anymore. We shared stories, laughter, even personal contact to stay in touch.</p>

<p>The man I never thought I could become—that’s who I turned out to be.</p>

<p>They saw it. They believed in me.</p>

<hr />

<p>The most recent doubt?</p>

<p>I thought I’d be stuck. Forever doing the same job in the same place. Every rejection from abroad felt like confirmation: You’re not good enough. Every email hurt. But I kept applying.</p>

<p>And now?</p>

<p>I landed offers abroad.</p>

<p>I packed up. I left.</p>

<p>I’m working in a place I’d never imagined.</p>

<p>It’s terrifying.</p>

<p>Maybe this is just another chapter in the self-doubt saga. Maybe the fight isn’t over.</p>

<p>But I’m still fighting.</p>

<hr />

<p>So… is self-doubt an unwinnable battle?</p>

<p>No.</p>

<p>Is it easy to win?</p>

<p>Hell no.</p>

<p>It’s painful. Relentless.</p>

<p>But I’ve learned this: Sometimes, all you need is one moment that proves you wrong. One little crack in the belief that you’ll never make it.</p>

<p>I’ve been lucky. I’ve had a few of those moments.</p>

<p>And I’ve had my family beside me through all of it. Without them, I might’ve missed my shot.</p>

<p>So I count my blessings.</p>

<p>And when doubt creeps in, I remember: the only way is forward.</p>

<p>You can fall. You can scream at the sky. You can hate yourself for a moment. But you get up.</p>

<p>And try again.</p>]]></content><author><name>Made Indra</name></author><category term="" /><summary type="html"><![CDATA[A personal journey of battling self-doubt, overcoming challenges, and finding moments of belief that change everything.]]></summary></entry><entry><title type="html">Tuntutlah Karir Sampai ke Negeri Seberang</title><link href="https://madeindra.dev/tuntutlah-karir-sampai-ke-negeri-seberang/" rel="alternate" type="text/html" title="Tuntutlah Karir Sampai ke Negeri Seberang" /><published>2025-04-27T00:00:00+00:00</published><updated>2025-04-27T00:00:00+00:00</updated><id>https://madeindra.dev/tuntutlah-karir-sampai-ke-negeri-seberang</id><content type="html" xml:base="https://madeindra.dev/tuntutlah-karir-sampai-ke-negeri-seberang/"><![CDATA[<p>Diskusi saya dengan teman-teman se-profesi seputar dunia kerja sering kali berujung pada pertanyaan, “Apakah mungkin kita bekerja sebagai <em>software engineer</em> sampai tua?”.</p>

<p>Pertanyaan ini valid karena sering kali <em>software engineer</em> di usia 30-40 tahun mulai beralih ke jalur manajerial. Tidak ada yang salah dengan pilihan tersebut, namun posisi manajerial bisa dibilang terbatas jika dibandingkan dengan jumlah <em>software engineer</em> yang ada di Indonesia saat ini.</p>

<p>Diskusi ini sering kali membuat salah satu dari kami berpikir untuk mencari pekerjaan di luar negeri. Mungkin karena merasa bahwa <em>demand</em> di sini belum sepadan dengan <em>supply</em> yang ada. Mungkin juga karena ingin memberikan kualitas hidup yang lebih baik untuk keluarga. Atau mungkin hanya sekadar ingin mencoba bekerja dan hidup di lingkungan yang baru.</p>

<p>Sebagai salah satu orang yang dulu sering memimpikan hal tersebut, saya ingin membagikan beberapa cara yang saya ketahui untuk mewujudkan mimpi-mimpi itu.</p>

<h2 id="bekerja-remote-untuk-perusahaan-luar-negeri">Bekerja Remote untuk Perusahaan Luar Negeri</h2>

<p>Bekerja secara remote bukanlah hal baru, dan banyak perusahaan luar negeri yang membuka lowongan pekerjaan untuk talenta di Indonesia.</p>

<p>Namun, yang jarang diketahui adalah bahwa cara ini juga bisa dimanfaatkan untuk pindah ke luar negeri.</p>

<p>Betul, pindah ke luar negeri. Bukan sekadar bekerja dari Indonesia.</p>

<p>Perusahaan luar negeri yang tidak memiliki cabang di Indonesia biasanya menawarkan dua cara untuk mempekerjakan orang dari negara lain: bekerja lepas (<em>freelance</em>) atau melalui perusahaan perantara.</p>

<p>Jika bekerja secara lepas, kita akan dibayar langsung oleh perusahaan tersebut. Namun, jika melalui perusahaan perantara, gaji kita akan disalurkan melalui perusahaan perantara tersebut. Kita juga bisa mendirikan perusahaan sendiri dalam bentuk CV atau PT, tetapi ada cara lain jika tujuan kita adalah pindah ke luar negeri.</p>

<p>Cara tersebut adalah melalui <strong>EoR</strong> (<em>Employer of Record</em>). Singkatnya, perusahaan EoR akan bertindak sebagai perantara. Secara hukum, kita akan tercatat sebagai karyawan perusahaan perantara tersebut, tetapi pekerjaan kita tetap langsung untuk perusahaan luar negeri yang membuka lowongan.</p>

<p>Beberapa contoh perusahaan EoR adalah Deel, Remote, Oyster, Employment Hero, dan Skuad.</p>

<p>Sebagai contoh, jika EoR memiliki cabang di Malaysia, kita bisa memilih untuk tercatat sebagai karyawan EoR di cabang Malaysia. Dengan begitu, segala urusan terkait visa kerja, pajak, penggajian, cuti, dan hari libur akan diurus oleh perusahaan EoR. Kita cukup mempersiapkan diri untuk bekerja dan hidup di Malaysia.</p>

<p>Keuntungan bagi perusahaan yang menggunakan EoR adalah adanya landasan hukum yang jelas, biaya yang terjangkau, dan kemudahan dalam pengelolaan karyawan. Sehingga kecil kemungkinan perusahaan menolak opsi ini.</p>

<p>Namun, ada keterbatasan dalam metode ini: perusahaan biasanya tidak menyesuaikan gaji berdasarkan lokasi yang kita pilih.</p>

<p>Sebagai contoh, jika perusahaan di Amerika Serikat menawarkan gaji sebesar $60.000, jumlah ini tidak akan berubah, baik kita memilih bekerja dari Indonesia maupun Malaysia. Akibatnya, gaji bersih yang kita terima bisa lebih kecil tergantung pada pajak dan biaya hidup di negara yang dituju.</p>

<p>Cara ini paling cocok digunakan jika kita mendapatkan tawaran pekerjaan dengan gaji yang besar, sehingga potongan pajak dan biaya hidup tidak menjadi masalah yang signifikan.</p>

<h2 id="menjadi-digital-nomad">Menjadi Digital Nomad</h2>

<p>Jika kita mendapatkan tawaran pekerjaan dengan gaji yang besar dan memilih untuk tetap bekerja dari Indonesia, masih ada cara untuk pindah ke luar negeri.</p>

<p>Cara tersebut adalah dengan memanfaatkan “<strong>Digital Nomad Visa</strong>”.</p>

<p>Uniknya, cara ini juga bisa ditempuh untuk pekerja lepas (<em>freelancer</em>).</p>

<p>Visa ini merupakan jenis visa yang ditawarkan kepada individu yang ingin bekerja dari suatu negara secara remote. Persyaratan visa ini berbeda-beda di setiap negara, tetapi umumnya mencakup jumlah minimum gaji per bulan dan jenis pekerjaan yang dapat dilakukan secara remote.</p>

<p>Keterbatasan visa digital nomad adalah adanya batas maksimal masa tinggal di negara tersebut. Sebagai contoh, kita mungkin hanya diizinkan tinggal dan bekerja di negara tersebut selama 6 bulan hingga 1 tahun.</p>

<p>Beberapa negara yang menawarkan visa ini antara lain <a href="https://www.canada.ca/en/immigration-refugees-citizenship/campaigns/high-skilled-workers.html">Kanada</a>, <a href="https://www.e-resident.gov.ee/nomadvisa/">Estonia</a>, <a href="https://www.exteriores.gob.es/Consulados/londres/en/ServiciosConsulares/Paginas/Consular/Digital-Nomad-Visa.aspx">Spanyol</a>, <a href="https://imin-portugal.com/blog/portugal-digital-nomad-visa/">Portugal</a>, <a href="https://www.thaiembassy.com/thailand/thailand-digital-nomad-visa-and-work-permit">Thailand</a>, <a href="https://www.mofa.go.jp/ca/fna/pagewe_000001_00046.html">Jepang</a>, <a href="https://overseas.mofa.go.kr/sg-en/brd/m_2444/view.do?seq=761493&amp;page=1">Korea Selatan</a>, dan masih banyak lainnya.</p>

<h2 id="mencari-kerja-di-negara-tujuan">Mencari Kerja di Negara Tujuan</h2>

<p>Tanpa tawaran pekerjaan, kita tetap memiliki peluang untuk pindah ke luar negeri dan mencari pekerjaan di sana.</p>

<p>Cara ini dapat dilakukan dengan memanfaatkan <strong>Job Seeker Visa</strong>.</p>

<p>Visa ini dirancang untuk individu yang memenuhi kriteria sebagai pencari kerja, biasanya di bidang-bidang yang sedang membutuhkan banyak tenaga kerja baru, salah satunya adalah bidang teknologi.</p>

<p>Beberapa negara yang menawarkan visa pencari kerja ini antara lain <a href="https://www.migrationsverket.se/en/you-want-to-apply/work/look-for-work/look-for-work-or-start-a-business.html">Swedia</a>, <a href="https://www.make-it-in-germany.com/en/visa-residence/types/job-search-opportunity-card">Jerman</a>, <a href="https://vistos.mne.gov.pt/en/national-visas/necessary-documentation/job-seeker-visa">Portugal</a>, <a href="https://www.migration.gv.at/en/types-of-immigration/permanent-immigration/very-highly-qualified-workers/">Austria</a>, dan <a href="https://u.ae/en/information-and-services/visa-and-emirates-id/visit-visas/jobseeker-visit-visa">UAE</a>.</p>

<p>Meskipun visa ini memberikan kemudahan karena tidak memerlukan tawaran pekerjaan sebelumnya, persyaratannya biasanya cukup ketat. Contohnya, pelamar harus memiliki pendidikan minimal setara sarjana dan sertifikat kemampuan bahasa tertentu.</p>

<p>Jika kita berhasil mendapatkan pekerjaan selama masa berlaku visa ini, kita harus melalui proses tambahan untuk mengajukan visa kerja baru. Dengan demikian, visa pencari kerja ini hanya menjadi langkah awal untuk memulai karir di luar negeri.</p>

<h2 id="sponsor-visa">Sponsor Visa</h2>

<p>Mendapatkan pekerjaan yang sekaligus memberikan sponsorship untuk visa kerja adalah salah satu cara yang paling menantang.</p>

<p>Kita harus bersaing dengan kandidat dari seluruh dunia, dan terkadang penawaran gaji yang diberikan mungkin tidak terlalu besar.</p>

<p>Salah satu trik yang saya ketahui adalah mencari lowongan di perusahaan-perusahaan yang memiliki izin resmi untuk memberikan sponsorship.</p>

<p>Contohnya: <a href="https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers">Inggris</a>, <a href="https://ind.nl/en/public-register-recognised-sponsors">Belanda</a>, dan <a href="https://www.homeaffairs.gov.au/foi/files/2024/fa-240100298-document-released.PDF">Australia</a>.</p>

<p>Dengan mengetahui daftar perusahaan-perusahaan tersebut terlebih dahulu, kita dapat memastikan bahwa perusahaan tersebut memiliki kemampuan untuk membantu proses pembuatan visa kerja.</p>

<p>Langkah berikutnya adalah membuka situs resmi perusahaan tersebut dan memeriksa apakah ada lowongan pekerjaan yang sesuai dengan keahlian kita.</p>

<blockquote>
  <p>Memang tidak mudah karena proses ini harus dilakukan satu per satu</p>

  <p>Namun, jalan menuju tempat yang indah memang terkadang tidak mudah.</p>
</blockquote>

<p>Beberapa negara, seperti Jerman dan Estonia, bahkan lebih fleksibel karena perusahaan di sana dapat memberikan visa kerja tanpa harus terdaftar sebelumnya sebagai pemberi sponsorship resmi.</p>

<h2 id="cara-lain">Cara Lain?</h2>

<p>Tidak ada jalan pintas untuk mendapatkan pekerjaan di luar negeri.</p>

<p>Hindari percaya pada pihak yang mengatasnamakan diri mereka sebagai agen pencari kerja, karena besar kemungkinan hal tersebut adalah penipuan.</p>

<p>Selalu lakukan riset mandiri secara mendetail, dan jangan mudah terbuai oleh mimpi-mimpi yang dijual oleh “influencer” di media sosial.</p>

<p>Yang paling penting, tetaplah semangat dan jangan mudah menyerah! Jika gagal, teruslah berusaha untuk memperbaiki diri. Suatu hari, jalan itu pasti akan terbuka! ✧⁺⸜(･ ᗜ ･ )⸝⁺✧</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@avirichards?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Avi Richards on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Work" /><summary type="html"><![CDATA[Berbagai cara yang bisa ditempuh untuk mendapatkan pekerjaan impian di luar negeri berdasar pengalaman saya sebagai software engineer dari Indonesia.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2025-04-27-tuntutlah-karir-sampai-ke-negeri-seberang.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2025-04-27-tuntutlah-karir-sampai-ke-negeri-seberang.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Banyak Jalan Menuju Jerman</title><link href="https://madeindra.dev/banyak-jalan-menuju-jerman/" rel="alternate" type="text/html" title="Banyak Jalan Menuju Jerman" /><published>2025-02-15T00:00:00+00:00</published><updated>2025-02-15T00:00:00+00:00</updated><id>https://madeindra.dev/banyak-jalan-menuju-jerman</id><content type="html" xml:base="https://madeindra.dev/banyak-jalan-menuju-jerman/"><![CDATA[<p>Setelah hampir 2 tahun bekerja secara remote untuk perusahaan di Australia, saya mulai merasa yakin bahwa saya mampu berdiri di panggung yang sama dengan software engineer dari seluruh dunia.</p>

<p>Sebelumnya, pekerjaan pertama saya adalah menjadi seorang dosen di sebuah kampus swasta kecil. Setahun kemudian, saya bergabung dengan perusahaan BUMN melalui jalur Management Trainee.</p>

<p>Meski telah meniti karir selama 5 tahun, saya merasa belum memberikan <em>impact</em> yang signifikan untuk bisa saya banggakan. Namun, 2 tahun terakhir benar-benar mengubah perjalanan hidup saya dengan pengalaman yang sangat berharga.</p>

<p>Di penghujung tahun 2024, saya mulai mencari informasi tentang cara berkarir di luar negeri.</p>

<hr />

<p>Tujuan saya bekerja di luar negeri adalah untuk mendapatkan kualitas hidup yang lebih baik, melihat dunia secara langsung, berinteraksi dengan orang-orang dari berbagai latar belakang, serta memberikan kontribusi yang lebih besar di bidang software development.</p>

<p>Negara-negara yang menjadi target utama saya adalah: Australia, Selandia Baru, Swedia, Estonia, dan Jerman.</p>

<p>Australia dan Selandia Baru menjadi pilihan karena setiap hari saya bekerja dengan rekan-rekan dari negara tersebut. Saya merasa cocok dengan budaya kerja mereka dan mampu membangun hubungan yang tidak hanya profesional tetapi juga bersifat personal.</p>

<p>Swedia dan Jerman menarik perhatian saya karena kedua negara ini menyediakan peluang besar bagi pencari kerja melalui program <a href="https://www.migrationsverket.se/English/Private-individuals/Working-in-Sweden/Look-for-work-or-start-a-business.html">Job Seeker Visa</a> dan <a href="https://www.make-it-in-germany.com/en/visa-residence/types/job-search-opportunity-card">Opportunity Card</a>. Program-program ini memungkinkan saya untuk datang ke negara tersebut tanpa memiliki pekerjaan sebelumnya. Peluang saya menjadi lebih banyak karena saya memiliki banyak opsi, bisa mengajukan visa pencari kerja, melamar pekerjaan secara langsung, atau mencari beasiswa doktoral. Hal unik dari Swedia adalah mahasiswa doktoral di sana dianggap sebagai pekerja penuh waktu, sehingga mereka mendapatkan gaji dan posisi ini dapat ditemukan di <em>job board</em> seperti LinkedIn atau Indeed.</p>

<p>Selain itu, ada negara dengan program serupa seperti <a href="https://vistos.mne.gov.pt/en/national-visas/necessary-documentation/job-seeker-visa">Portugal</a>, <a href="https://www.migration.gv.at/en/types-of-immigration/permanent-immigration/very-highly-qualified-workers/">Austria</a>, dan <a href="https://u.ae/en/information-and-services/visa-and-emirates-id/visit-visas/jobseeker-visit-visa">UAE</a>. Namun, peluang di negara-negara tersebut masih lebih sedikit atau kurang sesuai dengan kualifikasi saya jika dibandingkan negara prioritas saya.</p>

<p>Estonia menjadi target karena saya kagum dengan kemajuan teknologi digital di negara ini. Meskipun saya pernah gagal mendapatkan pekerjaan remote di perusahaan Estonia, mereka tetap memberikan saya penawaran <em>freelance</em> sebagai <em>expert</em>. Selain infrastruktur digitalnya yang maju, orang-orang Estonia sangat menghargai hasil kerja ketimbang waktu kerja. Satu-satunya kekurangan adalah kurangnya sinar matahari selama musim dingin.</p>

<p>Singkat cerita, setelah mengirim puluhan lamaran, saya mendapat 4 panggilan wawancara, 2 dari Jerman dan 2 dari Estonia. Setelah proses yang cukup panjang karena terbentur oleh libur Natal, saya mendapatkan 2 tawaran kerja: 1 dari perusahaan fintech di Jerman dan 1 lagi dari perusahaan SaaS CRM di Estonia.</p>

<p>Setelah mempertimbangkan berbagai aspek seperti budaya yang dimiliki perusahaan, peluang karir ke depannya, dan kualitas hidup untuk keluarga, saya memutuskan untuk menerima tawaran dari Jerman.</p>

<p>Lewat tulisan ini, saya ingin berbagi pengalaman mencari kerja di bidang IT ke luar negeri, khususnya Jerman, dan persiapan yang diperlukan.</p>

<h2 id="persiapan">Persiapan</h2>

<p>Karena nantinya kita akan mengajukan visa, langkah pertama yang saya sarankan adalah memahami persyaratan visa negara tujuan.</p>

<p>Untungnya, proses pengajuan visa Jerman saat ini sudah bisa dilakukan secara online lewat <a href="https://digital.diplo.de/">Auslandsportal</a>, kita cukup membuat akun di website tersebut, maka kita bisa melihat dokumen-dokumen yang dibutuhkan untuk mengajukan berbagai jenis visa, seperti visa kerja, visa pencari kerja, bahkan visa kumpul keluarga (<em>Family Reunification</em>) bagi pasangan, anak-anak, atau orang tua.</p>

<p>Dokumen yang diperlukan adalah:</p>
<ul>
  <li>Paspor</li>
  <li>Ijazah S1/D4 (hanya pekerja)</li>
  <li>Akta kelahiran</li>
  <li>Buku nikah atau akta perkawinan (untuk visa kumpul keluarga pasangan)</li>
  <li>Kartu keluarga (untuk visa kumpul keluarga anak sebagai bukti hak asuh anak)</li>
  <li>Sertifikat kemampuan bahasa Jerman C1 (untuk anak 16 tahun ke atas)</li>
  <li>SKCK (Surat Keterangan Catatan Kepolisian, hanya pekerja &amp; tergantung perusahaan)</li>
</ul>

<p>Pastikan dokumen-dokumen tersebut tidak hilang atau rusak dan tidak terdapat kesalahan. Jika ada kesalahan seperti kesalahan penulisan nama di ijazah atau nama orang tua di akta kelahiran, segera urus perbaikannya.</p>

<p>Syukurnya, bagi pemegang visa Blue Card, tidak ada syarat bahasa Jerman untuk pekerja maupun pasangan.</p>

<hr />

<p>Untuk pekerjaan bidang IT, syarat yang perlu diperhatikan adalah: pendidikan atau pengalaman kerja.</p>

<p>Lulusan S1/D4 bidang IT harus memastikan institusi pendidikan memiliki status “H+” atau “H+/H-“ di database Anabin, silakan cek <a href="https://www.make-it-in-germany.com/en/working-in-germany/recognition/academic-qualifications">panduan dari Make it in Germany</a>.</p>

<p>Jika institusi pendidikan dan program studi sama-sama memiliki status “H+”, pendidikan dianggap “<em>comparable</em>” dengan program yang diadakan di Jerman.</p>

<p>Jika program studi memiliki status “H+/H-“, nama institusi pendidikan harus muncul di daftar penyelenggara program studi. Jika tidak, kita diminta untuk meminta “<em>Statement of Comparability</em>” sebelum mengajukan visa. Pastikan institusi pendidikan terakreditasi Ban-PT, selama masa studi hingga tahun kelulusan, ini dibutuhkan nantinya saat pengajuan, silakan cek <a href="https://service.banpt.or.id/bianglala/bianglala.php">data histori akreditasi Ban-PT</a>.</p>

<p>Sayangnya jika salah satunya memiliki status “H-“, maka pendidikan tidak bisa digunakan. Proses pengajuan visa masih bisa dilakukan menggunakan pengalaman kerja, sama seperti jika memiliki latar pendidikan bukan dari bidang IT. Syaratnya adalah pengalaman kerja di bidang IT selama minimal 3 tahun dalam 7 tahun terakhir.</p>

<hr />

<p>Hal yang tidak kalah penting adalah mempersiapkan biaya pindah. Meskipun perusahaan nantinya akan memberikan <em>relocation assistance</em>, biasanya biaya bantuan akan diberikan setelah kita sampai di negara tujuan. Jadi, persiapkan dana pribadi terlebih dahulu.</p>

<p>Estimasi biaya:</p>
<ul>
  <li>IDR 650.000 per orang untuk paspor (jika perlu membuat / memperbarui)</li>
  <li>IDR 30.000 untuk SKCK (jika perlu membuat / memperbarui)</li>
  <li>IDR 150.000 per dokumen untuk apostille (pengganti legalisir, buku nikah tetap perlu dilegalisir sebelum apostille)</li>
  <li>IDR 200.000 hingga IDR 350.000 per halaman hasil terjemah untuk semua dokumen yang tidak berbahasa Inggris / Jerman</li>
  <li>IDR 7.000.000-17.000.000 per orang untuk tiket pesawat kelas ekonomi dengan 1x transit</li>
  <li>EUR 208 untuk <em>Statement of Comparability</em> bagi yang menggunakan ijazah S1/D4</li>
  <li>EUR 75 per orang untuk pengajuan visa</li>
  <li>EUR 1000 per orang untuk biaya hidup bulan pertama (menurut Numbeo)</li>
  <li>EUR 900 (1 orang) hingga EUR 2000 (3 orang) dikali 3 untuk biaya sewa apartemen (1 biaya bulan pertama dan deposit sebesar biaya sewa 2 bulan)</li>
</ul>

<p>Pengeluaran terbesar ada di sewa tempat tinggal karena apartemen di Jerman sangat terbatas. Kemungkinan beberapa bulan awal kita hanya bisa menyewa apartemen <em>furnished</em> karena untuk bisa menyewa apartemen <em>unfurnished</em>, kita akan diminta menyediakan laporan skor kredit (Schufa) kita. Jika membawa keluarga, jangan lupa ada syarat luas minimal tempat tinggal (12m² per orang atau 10m² untuk anak di bawah 6 tahun).</p>

<hr />

<p>Pastikan masa berlaku paspor masih lebih dari 6 bulan saat pengajuan visa dan minimal ada 2 halaman kosong.</p>

<p>Saat ini, pembuatan paspor sudah lebih sederhana karena pengisian data awal dilakukan lewat aplikasi M-Paspor. Jujur saja, <em>user experience</em> aplikasi ini tidak memuaskan karena banyak yang harus difoto, diupload, dan ditulis. Apalagi kalau keluar dari aplikasi di tengah-tengah proses, semua tahap harus diulang dari awal. Tetapi secara keseluruhan, menurut saya prosesnya lebih baik dibanding dulu dimana kita diharuskan datang ke kantor imigrasi untuk mengantri sebelum mengisi form kemudian harus menunggu beberapa jam untuk mendapat giliran interview dan foto. Sekarang, cukup isi data dan upload semua dokumen, kemudian pilih tanggal kedatangan sambil menyiapkan salinan dokumen-dokumen itu untuk dibawa ke kantor imigrasi.</p>

<p>Khusus untuk anak-anak (di bawah 5 tahun) atau lansia (di atas 60 tahun), tidak perlu mengisi data lewat M-Paspor, bisa langsung datang dan membawa berkas-berkas yang dipersyaratkan.</p>

<p>Pengalaman saya dari awal mendapat antrian konfirmasi aplikasi sampai interview dan foto menghabiskan waktu selama 1 jam. Setelah foto, saya diberikan bukti pengambilan yang juga berisi informasi cara pembayaran secara online. Paspor bisa diambil dalam waktu 4 hari kerja setelah pembayaran dilakukan, saat pengambilan pun antrian hanya menghabiskan waktu sekitar 30 menit, meskipun kondisi saat itu sedang ramai.</p>

<hr />

<p>Tidak semua perusahaan meminta bukti catatan kriminal, namun jika diminta, kita harus mengajukan SKCK ke Mabes Polri.</p>

<p>Mengapa harus di Mabes Polri? Karena tujuan penggunaan SKCK adalah untuk ke luar negeri, jadi tidak bisa di Sektor atau Resor, harus di pusat.</p>

<p>Sama seperti pembuatan paspor, pembuatan SKCK sekarang sudah lebih sederhana karena dilakukan lewat aplikasi POLRI Super App. Sama juga, <em>user experience</em> aplikasi ini tidak memuaskan terutama ketika proses pembayaran. Saya sendiri harus mengajukan sebanyak 3 kali di aplikasi tersebut karena di proses-proses sebelumnya nomor rekening virtual pembayaran tidak muncul.</p>

<p>Saya sangat menyarankan menggunakan metode pembayaran virtual account, jangan menggunakan metode pembayaran di loket karena dengan metode itu, kita harus mengantri di loket pembayaran sebelum SKCK dicetak.</p>

<p>Pengalaman saya dari awal mendapat antrian pengambilan SKCK sampai pencetakan selesai membutuhkan waktu tunggu selama 3 jam.</p>

<h2 id="mencari-kerja">Mencari Kerja</h2>

<p>Beberapa platform yang saya gunakan untuk mencari kerja:</p>
<ul>
  <li><a href="https://www.linkedin.com/">LinkedIn</a></li>
  <li><a href="https://www.xing.com/">Xing</a></li>
  <li><a href="https://www.make-it-in-germany.com/en/working-in-germany/job-listings">Make it in Germany</a></li>
  <li><a href="https://germantechjobs.de/">German Tech Jobs</a></li>
  <li><a href="https://www.joinimagine.com/">Imagine Foundation</a></li>
</ul>

<p>Saya spesifik menargetkan lowongan pekerjaan yang mencantumkan kata <strong>“relocation”</strong>. Jika lowongan pekerjaan tidak menyebutkan tentang bantuan relokasi secara eksplisit, saya akan mencari informasi jumlah karyawan dan <em>funding</em> perusahaan tersebut, jika cukup besar, saya akan tetap mengirim lamaran. Beberapa perusahaan yang tidak mencantumkan tentang bantuan relokasi juga menjawab lamaran saya dengan mengirimkan pertanyaan apakah saya siap untuk pindah jika diterima.</p>

<p>Peluang relokasi biasanya lebih besar jika kita melamar ke perusahaan besar (korporasi). Tapi, tidak menutup kemungkinan untuk diterima di perusahaan startup.</p>

<p>Ketika menggunakan LinkedIn atau Xing, saya menggunakan filter <em>Job Title</em> yang saya inginkan (contohnya “Software Engineer” atau “Backend”) dan <em>Location</em> (contohnya “Berlin”).</p>

<p>Jangan lupa, minimal gaji untuk bisa mengajukan visa Blue Card adalah EUR 43.759,80 (bidang IT, Dokter, Perawat, dan Insinyur).</p>

<p>Kurang lebih, semua <em>job board</em> memiliki model yang serupa, yaitu iklan lowongan kerja. Jadi persiapannya pun sama, yaitu: Resume / Curriculum Vitae (CV) dan Cover Letter.</p>

<p>Apakah cover letter wajib? Tidak! Namun, saya tetap merekomendasikan untuk menulis cover letter. Tidak perlu panjang, cukup yang singkat, padat, dan jelas.</p>

<p>Saya menggunakan template yang sama dengan resume, hanya saja isinya mencakup motivasi melamar, bukan riwayat kerja.</p>

<p>Hal yang perlu dituliskan di cover letter adalah:</p>
<ul>
  <li>Informasi pribadi (nama, posisi yang diinginkan, pengalaman kerja, pencapaian selama bekerja)</li>
  <li>Motivasi melamar (alasan mengapa kita cocok di posisi ini, termasuk budaya kerja / nilai dari perusahaan yang membuat kita tertarik)</li>
</ul>

<p>Saya sudah pernah membagikan template Resume dan Cover Letter, <a href="https://github.com/madecanggih/resource">silakan klik link GitHub ini</a>.</p>

<hr />

<p>Bicara tentang Imagine Foundation, platform ini sedikit berbeda dari <em>job board</em> lain. Imagine Foundation ini adalah organisasi non-profit yang membantu orang-orang dari negara berkembang untuk mendapatkan kerja di Jerman.</p>

<p>Cara kerjanya adalah kita mendaftar ke Imagine Foundation, kemudian kita akan mengikuti beberapa tahap seperti tes online, mengirimkan resume, memoles profil LinkedIn, dan mentoring. Kemudian jika kita lolos semua tahap tersebut, kita akan bergabung ke Talent Pool mereka.</p>

<p>Ada biaya awal sebelum bergabung sebesar EUR 20, ini dibayarkan setelah kita lolos semua tahap di atas, setelah itu kita akan dibantu mencari kerja.</p>

<p>Jika kita berhasil mendapatkan pekerjaan, kita harus membayar sebesar 7% dari gaji kotor tahun pertama, tapi pembayaran ini baru dilakukan setelah 6 bulan bekerja. Bisa dicicil selama 12 bulan atau dibayar sekaligus dengan potongan 10%.</p>

<p>Misalnya kita mendapatkan tawaran kerja dengan besaran gaji kotor sebesar EUR 60.000 per tahun, maka biaya yang kita bayar adalah EUR 4.200.</p>

<p>Saya sendiri berhasil melewati semua proses seleksi awal. Namun memilih untuk tidak bergabung ke talent pool karena saya sudah lebih dulu sudah mendapatkan tawaran kerja dari lowongan pekerjaan yang saya cari sendiri.</p>

<h2 id="apostille--terjemah">Apostille &amp; Terjemah</h2>

<p>Semua dokumen yang diterbitkan di Indonesia harus diterjemahkan ke dalam bahasa Jerman dan di-Apostille.</p>

<p>Pengajuan Apostille bisa dilakukan secara online lewat platform <a href="https://apostille.ahu.go.id/">Ditjen AHU Online</a>, butuh waktu 3 hari kerja untuk menunggu verifikasi dokumen.</p>

<p>Jika dokumen ditandatangani secara digital, proses akan lebih mudah.</p>

<p>Jika dokumen ditandatangan basah, maka kita mungkin perlu meminta spesimen tanda tangan pejabat tersebut atau pejabat yang baru.</p>

<p>Beruntung kalau data tanda tangan pejabat tersebut sudah ada di database Ditjen AHU. Jika belum, kita akan diberikan surat pengantar yang perlu dibawa langsung ke instansi untuk meminta spesimen tanda tangan. Jika pejabat sudah tidak menjabat karena dimutasi atau pensiun, kita cukup meminta pernyataan dari pejabat saat ini. Kedua formulir itu terlampir di surat pengantar yang diberikan.</p>

<p>Pengalaman saya, kadang meskipun data pejabat sudah terdaftar, bisa saja pengajuan apostille ditolak saat verifikasi. Cukup ajukan ulang, kecuali ada catatan lain dari verifikator (misal: salah input nomor dokumen atau nama).</p>

<p>Sebelum mengambil sertifikat apostille, pastikan semua dokumen sudah di-scan dengan baik. Jangan scan menggunakan kamera atau handphone untuk memastikan kualitas hasil scan yang baik. Hasil scan dokumen tersebut nantinya akan digunakan saat proses pengajuan visa. Selain itu, saat mengambil sertifikat apostille, dokumen asli dan sertifikat apostille akan digabung oleh petugas. Anehnya, cara penggabungannya adalah dengan menggunakan staples, namun jika dokumen asli dilaminasi, maka akan dilubangi terlebih dahulu dan digabungkan dengan menggunakan <em>eyelet</em> (mata ayam).</p>

<blockquote>
  <p>Sangat disayangkan bahwa proses apostille ini akan merusak dokumen asli.</p>

  <p>Saya tidak paham mengapa sertifikat apostille tidak dibuat dalam bentuk sertifikat digital yang juga menampung informasi dokumen aslinya.</p>
</blockquote>

<p>Saya tekankan lagi, scan semua dokumen dengan baik sebelum mengambil hasil apostille.</p>

<hr />

<p>Ada beberapa penerjemah yang menawarkan jasa terjemah lengkap dengan apostille, saya tidak merekomendasikan jasa tersebut karena harganya mahal. Selain itu, jasa tersebut biasanya tidak mencakup pengurusan spesimen tanda tangan, sehingga kita tetap harus melakukannya sendiri. Jasa apostille tersebut benar-benar hanya untuk mengurus pengajuan online dan pengambilan sertifikat.</p>

<p>Menurut saya, meskipun <em>user experience</em> website Apotille itu buruk, pengajuan apostille cukup mudah karena bisa dilakukan sepenuhnya secara online. Saat pengambilan, saya hanya mengajukan izin 1 hari, antrian di kantor pusat pun tidak begitu ramai. Lebih baik proses apostille dilakukan sendiri karena lebih hemat.</p>

<p>Saya sendiri mengalami kesulitan mendapatkan spesimen tanda tangan karena beberapa dokumen saya ditandatangani oleh pejabat yang berada di luar Jabodetabek. Bagi yang tidak memiliki saudara atau rekan untuk dimintakan tolong, pertimbangkan menggunakan bantuan jasa seperti <a href="https://santosuruh.co.id/">Santo Suruh</a>.</p>

<hr />

<p>Penerjemahan dokumen bisa dilakukan secara bersamaan dengan pengajuan apostille.</p>

<p>Penerjemah tersumpah mungkin akan memakan waktu lebih lama dan kita diharuskan mengirim dokumen asli.</p>

<p>Sejauh yang saya ketahui, tidak masalah menggunakan jasa penerjemah tidak tersumpah, asalkan penerjemah tersebut ada di dalam daftar penerjemah Kedutaan Besar. Silakan cek <a href="https://jakarta.diplo.de/id-de/service/2551760-2551760">daftar penerjemah di website resmi Kedutaan Besar Jerman</a></p>

<p>Saya sendiri menggunakan jasa penerjemah tidak tersumpah karena prosesnya lebih cepat dan cukup mengirimkan salinan dokumen asli.</p>

<h2 id="pengajuan-visa">Pengajuan Visa</h2>

<p>Proses pengajuan visa membutuhkan kontrak kerja yang sudah ditandatangani kedua belah pihak.</p>

<p>Selain melengkapi dokumen, kita juga diminta mengisi data diri atau istilahnya VIDEX, <a href="https://videx.diplo.de/videx/visum-erfassung/videx-langfristiger-aufenthalt">contoh formulir VIDEX dapat dilihat disini</a>.</p>

<p>Catatan: kemungkinan terdapat sedikit perbedaan dari contoh di atas dengan VIDEX yang perlu kita isi ketika pengajuan visa. Tidak perlu khawatir karena proses pengisian VIDEX dan upload dokumen nantinya akan sama-sama dilakukan di <a href="https://digital.diplo.de/">Auslandsportal</a>.</p>

<p>Panduan yang saya ikuti untuk mengisi VIDEX adalah panduan dari <a href="https://www.vfsglobal.com/Germany/Indonesia/pdf/User-guide-videx.pdf">VFS</a>.</p>

<h3 id="statement-of-comparability"><em>Statement of Comparability</em></h3>

<p>Jika perlu mengajukan <em>Statement of Comparability</em>, prosesnya bisa dilakukan melalui <a href="https://zab.kmk.org/en/statement-comparability">website Zentralstelle für ausländisches Bildungswesen (ZAB)</a>. Silakan ikuti <a href="https://www.youtube.com/watch?v=WyG-wM7gDNo">panduan dari Hello Germany</a>.</p>

<blockquote>
  <p>Saya mengalami kesulitan ketika melakukan login menggunakan Single Sign On (SSO) BundID.</p>

  <p>Ketika login berhasil dan tidak otomatis dialihkan ke website ZAB, logout terlebih dahulu di website BundID, kemudian ulangi proses login dari awal dengan mengklik Login di website ZAB.</p>
</blockquote>

<p>Dokumen yang diperlukan untuk pengajuan <em>Statement of Comparability</em>:</p>
<ul>
  <li>Ijazah S1/D4</li>
  <li>Transkrip nilai S1/D4</li>
  <li>Ijazah SMA</li>
  <li>Sertifikat akreditasi program studi dari Ban-PT selama masa studi</li>
  <li>Kontrak kerja (opsional)</li>
</ul>

<p>Jika ada versi ijazah dan transkrip nilai dalam bahasa Inggris, siapkan juga dokumen tersebut. Website ZAB menyediakan tempat upload terpisah untuk ijazah &amp; transkrip bahasa asli dan bahasa Inggris. Ini hanya berlaku untuk ijazah dan transkrip S1/D4. Jika tidak ada versi bahasa Inggris, dokumen asli tidak perlu diterjemahkan.</p>

<p>Kontrak kerja dibutuhkan untuk opsi Fast-Track. Normalnya proses penyetaraan menghabiskan waktu selama 3 bulan, namun dengan opsi Fast-Track pengajuan hanya menghabiskan waktu 2 minggu.</p>

<p>Setelah hasil penyetaraan selesai, periksa kembali dan pastikan tidak terdapat kesalahan. Jika terdapat kesalahan, cukup kirimkan pesan lewat website ZAB untuk perbaikan.</p>

<p>Hasil penyetaraan saya selesai kurang dari 2 minggu. Saat itu terdapat kesalahan di nama saya dan revisi kesalahan tersebut hanya membutuhkan waktu 1-2 hari kerja.</p>

<h3 id="asuransi-kesehatan">Asuransi Kesehatan</h3>

<p>Jika sudah ada kontrak kerja, saya sarankan untuk mendaftarkan asuransi kesehatan, karena dokumen asuransi perlu dilampirkan saat pengajuan visa.</p>

<p>Saya memilih mendaftar asuransi kesehatan TK (Techniker Krankenkasse) secara online. Karena asuransi kesehatan itu baru memulai mengcover saya setelah saya mulai bekerja, saya juga mengambil asuransi perjalanan dengan coverage 30 hari sebelum hari pertama bekerja. Yang saya tau, tidak ada ketentuan masa coverage, asalkan bisa mengcover dari hari pertama mendarat di Jerman sampai hari pertama bekerja.</p>

<p>Proses pendaftaran bisa dilakukan lewat <a href="https://www.expatrio.com/health-insurance-plus">Expatrio</a> atau <a href="https://feather-insurance.com/health-insurance/public">Feather</a> dan menghabiskan waktu 2 hari kerja.</p>

<h3 id="fast-track-untuk-visa-blue-card">Fast-Track untuk Visa Blue Card</h3>

<p>Jika perusahaan bersedia, mereka bisa mengajukan proses Fast-Track untuk pengajuan visa Blue Card. Proses ini dilakukan oleh perusahaan lewat Ausländerbehörde (<em>Foreigners authority</em>) sebelum kita mengajukan visa. Untuk informasi lebih lanjut silakan <a href="https://www.make-it-in-germany.com/en/looking-for-foreign-professionals/entering/the-fast-track-procedure-for-skilled-workers">klik link berikut</a>.</p>

<p>Proses ini dilakukan setelah ada kontrak kerja (minimal dalam bentuk <em>draft</em>) dan secara keseluruhan proses diperkirakan menghabiskan waktu 15 minggu, ini sudah termasuk 8 minggu untuk proses <em>Statement of Comparability</em>.</p>

<p>Jika proses ini dilakukan setelah memiliki <em>Statement of Comparability</em>, keputusan proses Fast-Track (persetujaun atau penolakan) diperkirakan akan diberikan dalam waktu 1 minggu.</p>

<p>Proses ini tetap bisa berjalan cukup lama, tergantung antrian pada Ausländerbehörde di negara bagian tujuan. Saya sendiri menunggu selama 4 minggu sebelum mendapat persetujuan.</p>

<p>Jika proses Fast-Track visa kita disetujui, kita harus mengajukan visa paling lambat 3 minggu setelah persetujuan dikeluarkan. Pengajuan visa visa Blue Card akan diproses lebih cepat dari biasanya, hanya dalam 3 minggu.</p>

<p>Dari informasi yang saya dapat, proses percepatan bisa juga berlaku untuk pasangan atau anak atau orang tua, jika mereka sebagai pemegang visa <em>Family Reunification</em> akan datang ke Jerman tidak lebih dari 6 bulan setelah kita sebagai pemegang visa Blue Card ada di Jerman.</p>

<p>Persetujuan dari Ausländerbehörde negara bagian ini disebut Pre-Approval (<em>Vorabzustimmung</em>), nantinya akan ada bagian khusus di form dokumen yang bisa diupload di Auslandsportal.</p>

<h3 id="janji-temu-visa">Janji Temu Visa</h3>

<p>Baik dengan proses fast-track ataupun tidak, tahap selanjutnya adalah pembuatan janji temu (<em>appointment</em>) visa.</p>

<p>Karena menggunakan proses fast-track, saya mendapat jawaban hanya dalam 1 minggu setelah mengupload semua dokumen. Jawaban bisa berupa permintaan revisi VIDEX atau dokumen atau langsung diterima.</p>

<p>Tampilan di Auslandsportal akan berubah dan menampilkan tombol “Make an appoinment” yang akan mengarahkan kita ke website termin milik Kedutaan Besar Jerman Jakarta.</p>

<p>Oh iya, saat itu saya melihat ada 2 pilihan link temu janji: National Visa &amp; National Visa for Family Reunification. Silakan pilih yang sesuai, lalu pilih tanggal dan isi nama lengkap &amp; nomor paspor.</p>

<p>Di Auslandsportal, terdapat daftar dokumen yang dibawa dengan pesan “data biometrik dan foto akan diambil saat janji temu”. Namun, pengalaman saya membuktikan kita tetap diminta membawa foto biometrik, jadi jangan lupa untuk menyiapkan 1 lembar foto biometrik sesuai <a href="https://digital.diplo.de/api/uploads/biometric_photos_instruction_0eb9a4741a.pdf">panduan kedubes</a>.</p>

<p>Catatan: lokasi foto biometrik terdekat dari kedubes ada di Jalan Sabang. Cukup datang ke salah satu studio foto dan bilang “Foto visa Eropa”.</p>

<p>Selain foto dan daftar dokumen dalam checklist, siapkan biaya EUR 75 untuk pembayaran visa. Meskipun ingin membayar dengan kartu kredit, tetap sediakan uang tunai dalam rupiah yang jumlahnya sama dengan nilai tersebut, karena sepengalaman saya, kartu saya tidak berhasil digunakan. Terdapat ATM BCA di dalam kedubes, namun lebih baik sediakan sebelum datang ke janji temu.</p>

<p>Saya sempat panik karena dalam checklist terdapat pesan larangan membawa perangkat elektronik saat janji temu, namun saat memasuki kedubes ternyata perangkat elektronik beserta benda yang tidak dibutuhkan akan disimpan di loker yang disediakan di sana.</p>

<hr />

<p>Janji temu cukup sederhana. Datang 15 menit sebelum jam yang ditentukan, di depan kedubes tidak terdapat tempat duduk ataupun tempat berteduh.</p>

<p>Setelah melewati pengecekan, barang-barang disimpan di loker, jadi kita cukup membawa dokumen yang dibutuhkan.</p>

<p>Naik ke lantai 2, ambil nomor antrian (tekan layar nomor antrian agak lama ya!) dan tunggu dipanggil. Prosesnya singkat, cukup menyerahkan dokumen dan foto, menyerahkan biaya visa, pengambilan sidik jari, selesai! Sebagai bukti, kita diberi kuitansi pembayaran biasa visa &amp; paspor akan diberi cap “National Visa Applied on xx xxx”.</p>

<p>Selama janji temu, saya hanya mendapat 1 pertanyaan, “Apakah keluarga akan ikut berangkat ke Jerman?”.</p>

<p>Setelah janji temu selesai, kita diminta menunggu sampai kedubes memberi keputusan visa. Status pengajuan di Auslandsportal mungkin tidak berubah selama kita menunggu hasil keputusan visa.</p>

<h3 id="cetak-visa">Cetak Visa</h3>

<p>Setelah 2 minggu, akhirnya saya mendapatkan email untuk mengambil visa ke kedubes.</p>

<p>Saya diminta untuk datang membawa paspor dan kuitansi pembayaran visa, proses ini bisa dilakukan setiap Senin-Jumat. Proses pengambilan paspor dilakukan di hari yang sama, kecuali hari Jumat.</p>

<p>Proses ini sangat singkat, kita hanya diminta melipat kuitansi dan diselipkan dalam paspor, kemudian keduanya diletakkan di dalam kotak di ruangan visa lantai 1.</p>

<p>Setelah itu kita kembali lagi setelah jam istirahat untuk mengambil visa. Nah, saat pengambilan ini kita diminta menunjukkan KTP karena paspor kita tidak ada.</p>

<p>Halaman paspor akan ditempel visa dan zusatzblatt yang menjelaskan ketentuan penggunaan visa.</p>

<hr />

<p>And that’s the final hurdle!</p>

<p>Selamat sudah mendapatkan visa dan sampai jumpa di Jerman! (￣▽￣)ノ</p>

<hr />

<p>Selama proses pengajuan visa, jika terdapat pertanyaan atau kesulitan dalam penggunaan Auslandsportal, ajukan lewat form <em>Contact</em> yang disediakan.</p>

<p>Setelah mengisi form <em>contact</em>, akan ada email balasan otomatis. Untuk mendapat jawaban dari tim support, balas email tersebut.</p>

<p>Saya menggunakan ChatGPT untuk membuat pesan dalam Bahasa Jerman. Bisa saja menggunakan Bahasa Inggris, namun saya merasa akan lebih mudah dipahami jika menggunakan bahasa asal mereka. Mungkin karena hal tersebut, saya jadi selalu mendapat jawaban dari tim support di hari yang sama saat saya membalas email.</p>

<p><strong>Kudos for the Auslandsportal support team!</strong></p>

<hr />

<p>Photo by <a href="https://unsplash.com/@florianwehde?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Florian Wehde on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Work" /><summary type="html"><![CDATA[Pengalaman saya mencari kerja ke luar negeri dan proses mendapatkan pekerjaan sebagai software engineer di Jerman, dari persiapan hingga mendapatkan visa kerja.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2025-02-15-banyak-jalan-menuju-jerman.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2025-02-15-banyak-jalan-menuju-jerman.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Money Data Type in Golang</title><link href="https://madeindra.dev/money-data-type-in-golang/" rel="alternate" type="text/html" title="Money Data Type in Golang" /><published>2024-12-09T12:00:00+00:00</published><updated>2024-12-09T12:00:00+00:00</updated><id>https://madeindra.dev/money-data-type-in-golang</id><content type="html" xml:base="https://madeindra.dev/money-data-type-in-golang/"><![CDATA[<p>It’s common knowledge that using <code class="language-plaintext highlighter-rouge">Float</code> (or <code class="language-plaintext highlighter-rouge">Number</code> in JavaScript, which is <code class="language-plaintext highlighter-rouge">Float64</code> behind the scenes) to store monetary data is a bad idea. If you don’t know this yet, try doing <code class="language-plaintext highlighter-rouge">10 / 3</code> in your choice of programming language. I’ve seen a lot of LinkedIn posts ridiculing JavaScript for this.</p>

<p><img src="/assets/others/floating-point-error-1.webp" alt="Dividing 10 by 3" /></p>

<p>When dealing with a user’s balance, let’s say in US Dollars, we sometimes need to deal with cent values. If a user has a balance of US$10.00 and we want to charge them 50 cents (US$0.50), they will have US$9.50 by the end of it. Doesn’t look wrong, right?</p>

<p>But what if the user only has US$1.00 and we charge them 70 cents (US$0.70)?</p>

<p><img src="/assets/others/floating-point-error-2.webp" alt="Subtracting 0.70 from 1.00" /></p>

<p>Mentally we already did the calculation and got US$0.30, but again when I tried the code in JavaScript, I got <code class="language-plaintext highlighter-rouge">0.30000000000000004</code>. Now the customer has more money than they should have.</p>

<blockquote>
  <p>Huh, weird?</p>

  <p>That couldn’t be right. Curse you JavaScript!</p>
</blockquote>

<p>I mean, this problem can be found not only in JavaScript but also in all programming languages that use the <a href="https://en.wikipedia.org/wiki/IEEE_754">IEEE 754</a> standard. So Golang has this problem too, run <a href="https://go.dev/play/p/WE25YQNwOZ5">this piece of code</a> if you don’t believe me.</p>

<h2 id="dealing-with-decimal-places">Dealing with Decimal Places</h2>

<blockquote>
  <p>So what should we use, then?</p>
</blockquote>

<p>Worry not! We have other data types.</p>

<p>Throughout my career, I’ve seen there are two ways most developers handle this; either by using <code class="language-plaintext highlighter-rouge">Integer</code> or <code class="language-plaintext highlighter-rouge">String</code>.</p>

<p>When using <code class="language-plaintext highlighter-rouge">Integer</code>, we write US$1 as <code class="language-plaintext highlighter-rouge">100</code>. Notice that there is no comma separator in the value. This way every calculation can be done in <code class="language-plaintext highlighter-rouge">Integer</code>, no more floating-point issues. This way, we will show <code class="language-plaintext highlighter-rouge">100</code> in API request/response and database to denote US$1.</p>

<p>When using <code class="language-plaintext highlighter-rouge">String</code>, we write US$1 as <code class="language-plaintext highlighter-rouge">"1.00"</code>, then we parse it to <code class="language-plaintext highlighter-rouge">Integer</code> so we get a value like the previous example, do some calculation, and make it into <code class="language-plaintext highlighter-rouge">String</code> again to be stored in the database. This way we can show <code class="language-plaintext highlighter-rouge">"1.00"</code> in the API request/response while calculating them fully in <code class="language-plaintext highlighter-rouge">Integer</code>.</p>

<p>To be honest, I am a bit opinionated on this money-data-type approach.</p>

<p>I think the <code class="language-plaintext highlighter-rouge">Integer</code> approach is great since there is no extra step necessary for parsing value, but the downside is the API consumer has to be aware that <code class="language-plaintext highlighter-rouge">100</code> means <code class="language-plaintext highlighter-rouge">US$1</code> and not <code class="language-plaintext highlighter-rouge">US$100</code>.</p>

<p>If you worked in backend, you know how silly it is to trust that the API consumer would have the same thought process as the API developer.</p>

<p>Personally, I liked the second approach where the API consumer can just pass <code class="language-plaintext highlighter-rouge">String</code> value of either <code class="language-plaintext highlighter-rouge">"1"</code> or <code class="language-plaintext highlighter-rouge">"1.00"</code> and we would still get the same <code class="language-plaintext highlighter-rouge">Integer</code> value of <code class="language-plaintext highlighter-rouge">100</code> to handle. But, I would store them as <code class="language-plaintext highlighter-rouge">Integer</code> not <code class="language-plaintext highlighter-rouge">String</code> so that parsing only happened when receiving request or before sending response.</p>

<h2 id="enter-the-json-interface">Enter the JSON Interface</h2>

<p>I would use JSON for most of my API, this is the format most consumers expect to get.</p>

<p>In Go, there are JSON interface methods that we can override to customize the behavior when receiving requests and sending responses. We will use this to eliminate the need to manually call the <code class="language-plaintext highlighter-rouge">String</code> to <code class="language-plaintext highlighter-rouge">Integer</code> parsing method every time.</p>

<h3 id="determining-the-number-of-decimal-places">Determining The Number of Decimal Places</h3>

<p>Before we start, let’s talk a little bit about currency.</p>

<p>Most countries use 2 decimal places. There’s a country like Kuwait that uses 3 decimal places. And there are countries like Indonesia and Japan that use no decimal places at all. I haven’t found any country that uses more than 3 decimal places, yet.</p>

<p>Being ambitious, let’s say we want to support all currencies from every country in the world. Standardizing 3 decimal places on the backend would be great. On the frontend we don’t need to care; <code class="language-plaintext highlighter-rouge">"1"</code>, <code class="language-plaintext highlighter-rouge">"1.0"</code>, <code class="language-plaintext highlighter-rouge">"1.00"</code> or <code class="language-plaintext highlighter-rouge">"1.000"</code> will all be treated as <code class="language-plaintext highlighter-rouge">"1.000"</code>. Less stress for the API consumers, less conflict we will have. LOL!</p>

<h3 id="overriding-json-marshalunmarshal">Overriding JSON Marshal/Unmarshal</h3>

<p>Now that we have agreed to use 3 decimal places for all money data, let’s start by coding the implementation.</p>

<p>This is our starting point, just like all other Go Projects:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>First, let’s create a custom data type:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Money</span> <span class="kt">int64</span>
</code></pre></div></div>

<p>Notice that we didn’t write it as <code class="language-plaintext highlighter-rouge">type Money = int64</code>, because this is Type Definition, not Type Alias.</p>

<p>The difference is when doing Aliasing, both data type are interchangeable, meaning we can pass <code class="language-plaintext highlighter-rouge">int64</code> to functions that expect <code class="language-plaintext highlighter-rouge">Money</code> if it were alias of <code class="language-plaintext highlighter-rouge">int64</code>. But, in Type Definition, we need to explicitly cast <code class="language-plaintext highlighter-rouge">int64</code> to <code class="language-plaintext highlighter-rouge">Money</code> before we can pass it to functions that expect <code class="language-plaintext highlighter-rouge">Money</code>.</p>

<p>Now that we have our custom data type, let’s add 2 methods to change the way we receive JSON request and send JSON response:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// UnmarshalJSON implements the json.Unmarshaler interface</span>
<span class="k">func</span> <span class="p">(</span><span class="n">m</span> <span class="o">*</span><span class="n">Money</span><span class="p">)</span> <span class="n">UnmarshalJSON</span><span class="p">(</span><span class="n">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// remove quotes from string</span>
	<span class="n">s</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">Trim</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="s">"</span><span class="se">\"</span><span class="s">"</span><span class="p">)</span>

	<span class="c">// if negative, remove sign</span>
	<span class="n">isNegative</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">HasPrefix</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"-"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">isNegative</span> <span class="p">{</span>
		<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="m">1</span><span class="o">:</span><span class="p">]</span>
	<span class="p">}</span>

	<span class="c">// split by point</span>
	<span class="n">parts</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">Split</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"."</span><span class="p">)</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">2</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid decimal format: %s"</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// parse leading part</span>
	<span class="n">intPart</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">strconv</span><span class="o">.</span><span class="n">ParseInt</span><span class="p">(</span><span class="n">parts</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="m">10</span><span class="p">,</span> <span class="m">64</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid integer part: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// multiply by 1000, later will add the trailing part</span>
	<span class="n">result</span> <span class="o">:=</span> <span class="n">intPart</span> <span class="o">*</span> <span class="m">1000</span>

	<span class="c">// parse trailing part, only if exist</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">==</span> <span class="m">2</span> <span class="p">{</span>
		<span class="c">// trim to at max 3 digits (additional decimal places will be ignored)</span>
		<span class="n">decimalPart</span> <span class="o">:=</span> <span class="n">parts</span><span class="p">[</span><span class="m">1</span><span class="p">]</span>
		<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">3</span> <span class="p">{</span>
			<span class="n">decimalPart</span> <span class="o">=</span> <span class="n">decimalPart</span><span class="p">[</span><span class="o">:</span><span class="m">3</span><span class="p">]</span>
		<span class="p">}</span>

		<span class="c">// pad with additional zeros (if less than 3 decimal places)</span>
		<span class="k">for</span> <span class="nb">len</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">)</span> <span class="o">&lt;</span> <span class="m">3</span> <span class="p">{</span>
			<span class="n">decimalPart</span> <span class="o">+=</span> <span class="s">"0"</span>
		<span class="p">}</span>

		<span class="c">// parse</span>
		<span class="n">decimal</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">strconv</span><span class="o">.</span><span class="n">ParseInt</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">,</span> <span class="m">10</span><span class="p">,</span> <span class="m">64</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid decimal part: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="p">}</span>

		<span class="c">// combine leading and trailing part</span>
		<span class="n">result</span> <span class="o">+=</span> <span class="n">decimal</span>
	<span class="p">}</span>

	<span class="c">// if negative, return back sign</span>
	<span class="k">if</span> <span class="n">isNegative</span> <span class="p">{</span>
		<span class="n">result</span> <span class="o">=</span> <span class="o">-</span><span class="n">result</span>
	<span class="p">}</span>

	<span class="o">*</span><span class="n">m</span> <span class="o">=</span> <span class="n">Money</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="c">// MarshalJSON implements the json.Marshaler interface</span>
<span class="k">func</span> <span class="p">(</span><span class="n">m</span> <span class="n">Money</span><span class="p">)</span> <span class="n">MarshalJSON</span><span class="p">()</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="c">// get non-negative value</span>
	<span class="n">value</span> <span class="o">:=</span> <span class="kt">int64</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
	<span class="n">sign</span> <span class="o">:=</span> <span class="s">""</span>
	<span class="k">if</span> <span class="n">value</span> <span class="o">&lt;</span> <span class="m">0</span> <span class="p">{</span>
		<span class="n">sign</span> <span class="o">=</span> <span class="s">"-"</span>
		<span class="n">value</span> <span class="o">=</span> <span class="o">-</span><span class="n">value</span>
	<span class="p">}</span>

	<span class="c">// calculate leading and trailing digits</span>
	<span class="n">intPart</span> <span class="o">:=</span> <span class="n">value</span> <span class="o">/</span> <span class="m">1000</span>
	<span class="n">decPart</span> <span class="o">:=</span> <span class="n">value</span> <span class="o">%</span> <span class="m">1000</span>

	<span class="c">// always format to trailing 3 decimal</span>
	<span class="n">str</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s%d.%03d"</span><span class="p">,</span> <span class="n">sign</span><span class="p">,</span> <span class="n">intPart</span><span class="p">,</span> <span class="n">decPart</span><span class="p">)</span>

	<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">str</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For request/response simulation, we will need a struct that has <code class="language-plaintext highlighter-rouge">Money</code> data type as the property:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Data</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Value</span> <span class="n">Money</span> <span class="s">`json:"value"`</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now to simulate receiving request:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// receiving request</span>
<span class="n">incoming</span> <span class="o">:=</span> <span class="s">`{"value": "1"}`</span>

<span class="k">var</span> <span class="n">received</span> <span class="n">Data</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">incoming</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">received</span><span class="p">)</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"incoming data parsed as int64:"</span><span class="p">,</span> <span class="n">received</span><span class="p">)</span>
</code></pre></div></div>

<p>The result will look like this:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>incoming data parsed as int64: <span class="o">{</span>1000<span class="o">}</span>
</code></pre></div></div>

<p>Whatever value being sent, be it <code class="language-plaintext highlighter-rouge">"1"</code>, <code class="language-plaintext highlighter-rouge">"1.0"</code>, <code class="language-plaintext highlighter-rouge">"1.00"</code> or <code class="language-plaintext highlighter-rouge">"1.000"</code>, the backend will receive it as <code class="language-plaintext highlighter-rouge">1000</code>. Neat, huh?</p>

<p>Now to simulate sending response:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// sending response</span>
<span class="n">outgoing</span> <span class="o">:=</span> <span class="n">Data</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">5000</span><span class="p">}</span>
<span class="n">sent</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">outgoing</span><span class="p">)</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"outgoing data converted to string:"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">sent</span><span class="p">))</span>
</code></pre></div></div>

<p>The result will look like this:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>outgoing data converted to string: <span class="o">{</span><span class="s2">"value"</span>:<span class="s2">"5.000"</span><span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Money</code> data type will be automatically converted to string. Perfect!</p>

<h3 id="putting-them-up">Putting Them Up</h3>

<p>The final result should looks like this:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"encoding/json"</span>
	<span class="s">"fmt"</span>
	<span class="s">"strconv"</span>
	<span class="s">"strings"</span>
<span class="p">)</span>

<span class="k">type</span> <span class="n">Money</span> <span class="kt">int64</span>

<span class="k">func</span> <span class="p">(</span><span class="n">m</span> <span class="o">*</span><span class="n">Money</span><span class="p">)</span> <span class="n">UnmarshalJSON</span><span class="p">(</span><span class="n">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// remove quotes from string</span>
	<span class="n">s</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">Trim</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="s">"</span><span class="se">\"</span><span class="s">"</span><span class="p">)</span>

	<span class="c">// if negative, remove sign</span>
	<span class="n">isNegative</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">HasPrefix</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"-"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">isNegative</span> <span class="p">{</span>
		<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="m">1</span><span class="o">:</span><span class="p">]</span>
	<span class="p">}</span>

	<span class="c">// split by point</span>
	<span class="n">parts</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">Split</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"."</span><span class="p">)</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">2</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid decimal format: %s"</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// parse leading part</span>
	<span class="n">intPart</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">strconv</span><span class="o">.</span><span class="n">ParseInt</span><span class="p">(</span><span class="n">parts</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="m">10</span><span class="p">,</span> <span class="m">64</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid integer part: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// multiply by 1000, later will add the trailing part</span>
	<span class="n">result</span> <span class="o">:=</span> <span class="n">intPart</span> <span class="o">*</span> <span class="m">1000</span>

	<span class="c">// parse trailing part, only if exist</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">==</span> <span class="m">2</span> <span class="p">{</span>
		<span class="c">// trim to at max 3 digits (additional decimal places will be ignored)</span>
		<span class="n">decimalPart</span> <span class="o">:=</span> <span class="n">parts</span><span class="p">[</span><span class="m">1</span><span class="p">]</span>
		<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">3</span> <span class="p">{</span>
			<span class="n">decimalPart</span> <span class="o">=</span> <span class="n">decimalPart</span><span class="p">[</span><span class="o">:</span><span class="m">3</span><span class="p">]</span>
		<span class="p">}</span>

		<span class="c">// pad with additional zeros (if less than 3 decimal places)</span>
		<span class="k">for</span> <span class="nb">len</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">)</span> <span class="o">&lt;</span> <span class="m">3</span> <span class="p">{</span>
			<span class="n">decimalPart</span> <span class="o">+=</span> <span class="s">"0"</span>
		<span class="p">}</span>

		<span class="c">// parse</span>
		<span class="n">decimal</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">strconv</span><span class="o">.</span><span class="n">ParseInt</span><span class="p">(</span><span class="n">decimalPart</span><span class="p">,</span> <span class="m">10</span><span class="p">,</span> <span class="m">64</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid decimal part: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="p">}</span>

		<span class="c">// combine leading and trailing part</span>
		<span class="n">result</span> <span class="o">+=</span> <span class="n">decimal</span>
	<span class="p">}</span>

	<span class="c">// if negative, return back sign</span>
	<span class="k">if</span> <span class="n">isNegative</span> <span class="p">{</span>
		<span class="n">result</span> <span class="o">=</span> <span class="o">-</span><span class="n">result</span>
	<span class="p">}</span>

	<span class="o">*</span><span class="n">m</span> <span class="o">=</span> <span class="n">Money</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="c">// MarshalJSON implements the json.Marshaler interface</span>
<span class="k">func</span> <span class="p">(</span><span class="n">m</span> <span class="n">Money</span><span class="p">)</span> <span class="n">MarshalJSON</span><span class="p">()</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="c">// get non-negative value</span>
	<span class="n">value</span> <span class="o">:=</span> <span class="kt">int64</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
	<span class="n">sign</span> <span class="o">:=</span> <span class="s">""</span>
	<span class="k">if</span> <span class="n">value</span> <span class="o">&lt;</span> <span class="m">0</span> <span class="p">{</span>
		<span class="n">sign</span> <span class="o">=</span> <span class="s">"-"</span>
		<span class="n">value</span> <span class="o">=</span> <span class="o">-</span><span class="n">value</span>
	<span class="p">}</span>

	<span class="c">// calculate leading and trailing digits</span>
	<span class="n">intPart</span> <span class="o">:=</span> <span class="n">value</span> <span class="o">/</span> <span class="m">1000</span>
	<span class="n">decPart</span> <span class="o">:=</span> <span class="n">value</span> <span class="o">%</span> <span class="m">1000</span>

	<span class="c">// always format to trailing 3 decimal</span>
	<span class="n">str</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s%d.%03d"</span><span class="p">,</span> <span class="n">sign</span><span class="p">,</span> <span class="n">intPart</span><span class="p">,</span> <span class="n">decPart</span><span class="p">)</span>

	<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">str</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">Data</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Value</span> <span class="n">Money</span> <span class="s">`json:"value"`</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="c">// receiving request</span>
	<span class="n">incoming</span> <span class="o">:=</span> <span class="s">`{"value": "1"}`</span>

	<span class="k">var</span> <span class="n">received</span> <span class="n">Data</span>
	<span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">incoming</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">received</span><span class="p">)</span>

	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"incoming data parsed as int64:"</span><span class="p">,</span> <span class="n">received</span><span class="p">)</span>

	<span class="c">// sending response</span>
	<span class="n">outgoing</span> <span class="o">:=</span> <span class="n">Data</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">5000</span><span class="p">}</span>
	<span class="n">sent</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">outgoing</span><span class="p">)</span>

	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"outgoing data converted to string:"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">sent</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="closing-up">Closing Up</h2>

<p>Now you know (arguably) the best way to handle money data in Golang.</p>

<p>Joke aside, this approach is not the perfect (yet), for production you might want to add more handling such as arithmetic operations, validation rules, and formatting with currency.</p>

<p>But, so far, using this approach, you already get:</p>
<ul>
  <li>Consistent decimal places handling</li>
  <li>Clean API request/response with string representation</li>
  <li>No floating-point arithmetic errors</li>
</ul>

<p>I’d say that’s already enough for a starter (๑˃ᴗ˂)ﻭ</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@nbmat?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Mathieu Turle on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Development" /><summary type="html"><![CDATA[Learn how to handle money data type in Golang without floating-point arithmetic errors using custom type definition and JSON interface implementation.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2024-12-09-money-data-type-in-golang.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2024-12-09-money-data-type-in-golang.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Storing Docker Image on GitHub Container Registry</title><link href="https://madeindra.dev/storing-docker-image-on-github-container-registry/" rel="alternate" type="text/html" title="Storing Docker Image on GitHub Container Registry" /><published>2024-12-08T00:00:00+00:00</published><updated>2024-12-08T00:00:00+00:00</updated><id>https://madeindra.dev/storing-docker-image-on-github-container-registry</id><content type="html" xml:base="https://madeindra.dev/storing-docker-image-on-github-container-registry/"><![CDATA[<p>Months ago I posted about using <a href="../shell-scripting-cicd">Shell Script for CI/CD</a> in a personal project in which the script includes cloning the repo locally, building the Docker image, and running it as a container right in the VPS. I know, I know. It’s not ideal, and it’s leaking the source code to the server, so let’s redo it.</p>

<p>I want to focus on using GitHub Actions on a private repository to build a Docker image and store it on GitHub Container Registry (<a href="https://ghcr.io/">ghcr.io</a>). It’s perfect for personal projects since we can use up to 500MB total for all images on the GitHub Free plan - it’s counted as GitHub Package storage usage, by the way.</p>

<p>What I am looking for in this round is:</p>
<ul>
  <li>Build the Docker image on GitHub Actions.</li>
  <li>Push the image into GitHub Container Registry.</li>
  <li>Pull the image into the server.</li>
  <li>Run a script to start the image as container on the server.</li>
</ul>

<p>Let’s take a look at how the action looks like.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">CI/CD Pipeline</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">${{ github.repository }}</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>

    <span class="c1"># this sets permission to read repo content and push image to ghcr.io</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="c1"># 1. Checkout the repository</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="c1"># 2. Set up Go</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Go</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-go@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">go-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.22'</span>

      <span class="c1"># 3. Install dependencies</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">go get -v -t -d ./...</span>

      <span class="c1"># 4. Run Golang tests</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run tests</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">go test -v ./...</span>
        
      <span class="c1"># 5. Login to ghcr.io</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Login to the container registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">${{ env.REGISTRY }}</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">${{ github.actor }}</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">${{ secrets.GITHUB_TOKEN }}</span>
      
      <span class="c1"># 6. Extract metadata (tags, labels) for Docker</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract metadata</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">meta</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/metadata-action@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">images</span><span class="pi">:</span> <span class="s">${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}</span>

      <span class="c1"># 7. Build and push image to ghcr.io</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push docker image</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/build-push-action@v6</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
          <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">tags</span><span class="pi">:</span> <span class="s">${{ steps.meta.outputs.tags }}</span>
          <span class="na">labels</span><span class="pi">:</span> <span class="s">${{ steps.meta.outputs.labels }}</span>
          <span class="c1"># you can pass build-args and secrets here</span>
          <span class="c1"># see https://github.com/marketplace/actions/build-and-push-docker-images</span>

  <span class="na">deploy</span><span class="pi">:</span>
    <span class="c1"># this prevent deployment when image building failed</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
    <span class="c1"># 8. SSH to the server and run the script</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">executing remote ssh commands using password</span>
      <span class="na">uses</span><span class="pi">:</span> <span class="s">appleboy/ssh-action@v1.2.0</span>
      <span class="na">with</span><span class="pi">:</span>
        <span class="na">host</span><span class="pi">:</span> <span class="s">${{ secrets.HOST }}</span>
        <span class="na">username</span><span class="pi">:</span> <span class="s">${{ secrets.USERNAME }}</span>
        <span class="na">password</span><span class="pi">:</span> <span class="s">${{ secrets.PASSWORD }}</span>
        <span class="na">port</span><span class="pi">:</span> <span class="s">${{ secrets.PORT }}</span>
        <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># log in to container registry</span>
          <span class="s">echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin</span>
          
          <span class="s"># pull the image</span>
          <span class="s">docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest</span>
          
          <span class="s"># export necessary environment variables for the script here</span>
          <span class="s"># export VARIABLE_NAME=value</span>

          <span class="s"># download and run deploy script using curl with auth header</span>
          <span class="s">curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \</span>
               <span class="s">-H "Accept: application/vnd.github.v3.raw" \</span>
               <span class="s">-o /vps/path/to/save/script_name.sh \</span>
               <span class="s">-L "https://api.github.com/repos/${{ github.repository }}/contents/repo/path/to/script_name.sh?ref=${{ github.sha }}"</span>

          <span class="s">chmod +x /vps/path/to/save/script_name.sh</span>
          <span class="s">/vps/path/to/save/script_name.sh</span>

          <span class="s"># remove script after run completed</span>
          <span class="s">rm -f /vps/path/to/save/script_name.sh</span>
</code></pre></div></div>

<p>With the above changes, no more cloning is needed to build images on the server.</p>

<p>All the build processes happen in the pipeline, and when it’s successfully built, the SSH script will log in to ghcr.io and pull the image. I chose to do Docker login before the script to prevent passing the <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> secret to the script.</p>

<blockquote>
  <p>P.S. <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> is a provided secret value.
No need to add them to the repository secret.</p>

  <p>The only thing I did was adding secrets for the ssh step.</p>
</blockquote>

<p>Only the script is downloaded into the server; the rest of the source code is not. This way, the script can be safely stored in the same repository. Moreover, it’s deleted from the server after the run is completed. Neat!</p>

<p>Just like the previous post, you can do anything inside the <code class="language-plaintext highlighter-rouge">script_name.sh</code>. The only difference would be that this time it would be simpler without the Docker build step:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="c"># Name for the Docker image</span>
<span class="nv">IMAGE_NAME</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">:-</span><span class="nv">username</span><span class="p">/imagename</span>:latest<span class="k">}</span><span class="s2">"</span>
<span class="nv">CONTAINER_NAME</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_NAME</span><span class="k">:-</span><span class="nv">containername</span><span class="k">}</span><span class="s2">"</span>

<span class="c"># Check if a container with CONTAINER_NAME exists and stop/remove it</span>
<span class="nv">EXISTING_CONTAINER</span><span class="o">=</span><span class="si">$(</span>docker ps <span class="nt">-aq</span> <span class="nt">-f</span> <span class="nv">name</span><span class="o">=</span><span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span><span class="si">)</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$EXISTING_CONTAINER</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Stopping and removing existing container with name </span><span class="nv">$CONTAINER_NAME</span><span class="s2">..."</span>
    docker stop <span class="nv">$CONTAINER_NAME</span>
    docker <span class="nb">rm</span> <span class="nv">$CONTAINER_NAME</span>
<span class="k">fi</span>

<span class="c"># Run Docker container</span>
<span class="nb">echo</span> <span class="s2">"Running Docker container..."</span>
docker run <span class="nt">-d</span> <span class="nt">--restart</span> always <span class="nt">--name</span> <span class="nv">$CONTAINER_NAME</span> <span class="nv">$IMAGE_NAME</span>

<span class="nb">echo</span> <span class="s2">"Docker container started."</span>

</code></pre></div></div>

<p>Finally, you might be wondering whether GitHub has a dashboard for your images like what <a href="https://hub.docker.com/">DockerHub</a> does. Worry not! You can go to <a href="https://github.com/username?tab=packages">https://github.com/username?tab=packages</a> (change it to your username) to see a list of images you have stored in ghcr.io.</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@richygreat?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Richy Great on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Development" /><summary type="html"><![CDATA[Learn how to use GitHub Actions to build Docker images and store them in GitHub Container Registry (ghcr.io). A guide to improving CI/CD workflow for private repositories without exposing source code to servers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2024-12-08-storing-docker-image-on-github-container-registry.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2024-12-08-storing-docker-image-on-github-container-registry.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Docker Argument vs Variable vs Secret</title><link href="https://madeindra.dev/docker-argument-vs-variable-vs-secret/" rel="alternate" type="text/html" title="Docker Argument vs Variable vs Secret" /><published>2024-12-07T00:00:00+00:00</published><updated>2024-12-07T00:00:00+00:00</updated><id>https://madeindra.dev/docker-argument-vs-variable-vs-secret</id><content type="html" xml:base="https://madeindra.dev/docker-argument-vs-variable-vs-secret/"><![CDATA[<p>You might be wondering how to pass a value while building a Docker image. I’ve been there; in fact, I still revisit this topic from time to time. So I want to dedicate a post to building an image with arguments, variables, and secrets.</p>

<p>Let’s start with a simple Dockerfile with multiple stages:</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> golang:1.23</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> &lt;&lt;EOF ./main.go</span>
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
<span class="k">RUN </span>go build <span class="nt">-o</span> /bin/hello ./main.go

<span class="k">FROM</span><span class="s"> scratch</span>
<span class="k">COPY</span><span class="s"> --from=0 /bin/hello /bin/hello</span>
<span class="k">CMD</span><span class="s"> ["/bin/hello"]</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">FROM</code> statements you saw in the example above define stages. The first one builds the file, and the second runs the built binary.</p>

<p>Now let’s talk about build arguments and environment variables. Build arguments in Docker are closely related to environment variables; they are written as <code class="language-plaintext highlighter-rouge">ARG</code> and <code class="language-plaintext highlighter-rouge">ENV</code>, respectively. They are used to pass information to the build process.</p>

<h2 id="build-arguments">Build Arguments</h2>

<p>The way you use <code class="language-plaintext highlighter-rouge">ARG</code> is by defining it either before the stage (a.k.a Global Scope) or inside the stage (a.k.a Local Scope).</p>

<p>The benefit of using <code class="language-plaintext highlighter-rouge">ARG</code> is we can build the image in different way without editing the <code class="language-plaintext highlighter-rouge">Dockerfile</code>.</p>

<p>The value of <code class="language-plaintext highlighter-rouge">ARG</code> is only available during the build process and will not be available when the image is running as a container, unless it is explicitly passed.</p>

<p>I recommend using <code class="language-plaintext highlighter-rouge">ARG</code> for configuring builds. For example, to build a different version, or to use different base image.</p>

<p>This is an example of using <code class="language-plaintext highlighter-rouge">ARG</code> to define the base image to be used for the build.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create argument for image version</span>
<span class="k">ARG</span><span class="s"> GO_VERSION="1.23"</span>

<span class="c"># use argmument</span>
<span class="k">FROM</span><span class="s"> golang:${GO_VERSION}</span>

<span class="c"># rest of the file stay the same as first example</span>
...
</code></pre></div></div>

<p>Try to build the example above, and you will see that it uses <code class="language-plaintext highlighter-rouge">golang:1.23</code></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> example:latest <span class="nb">.</span>
</code></pre></div></div>

<p>But if you pass a different version, let’s say <code class="language-plaintext highlighter-rouge">1.22</code>, it will use <code class="language-plaintext highlighter-rouge">golang:1.22</code> instead.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">--build-arg</span> <span class="nv">GO_VERSION</span><span class="o">=</span><span class="s2">"1.22"</span> <span class="nt">-t</span> example:latest <span class="nb">.</span>
</code></pre></div></div>

<h3 id="without-default-value">Without Default Value</h3>

<p>We can also just set the argument name without defining the value. This way, the value of the argument will be empty if not provided in the build command.</p>
<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create argument without default value</span>
<span class="k">ARG</span><span class="s"> GO_VERSION</span>

<span class="c"># use argmument</span>
<span class="k">FROM</span><span class="s"> golang:${GO_VERSION}</span>

<span class="c"># rest of the file stay the same as first example</span>
...
</code></pre></div></div>

<h3 id="scope">Scope</h3>

<p>If we declare <code class="language-plaintext highlighter-rouge">ARG</code> in the global scope and want to use it inside a stage, <code class="language-plaintext highlighter-rouge">ARG</code> has to be redeclared inside the stage to allow the stage to read the value. There’s no need to write the value again; just the name is enough for the stage to inherit the value.</p>

<p>This version will not work</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> MESSAGE="helo, world"</span>

<span class="k">FROM</span><span class="s"> golang:1.23</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="c"># this will fail</span>
<span class="k">COPY</span><span class="s"> &lt;&lt;EOF ./main.go</span>
package main

import "fmt"

func main() {
  fmt.Println("${MESSAGE}")
}
EOF
<span class="k">RUN </span>go build <span class="nt">-o</span> /bin/hello ./main.go

<span class="c"># rest of the file stay the same as first example</span>
...
</code></pre></div></div>

<p>With redeclaration, it now works perfectly.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> MESSAGE="helo, world"</span>

<span class="k">FROM</span><span class="s"> golang:1.23</span>
<span class="c"># inherit the arg from global scope</span>
<span class="k">ARG</span><span class="s"> MESSAGE </span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> &lt;&lt;EOF ./main.go</span>
package main

import "fmt"

func main() {
  fmt.Println("${MESSAGE}")
}
EOF
<span class="k">RUN </span>go build <span class="nt">-o</span> /bin/hello ./main.go

<span class="c"># rest of the file stay the same as first example</span>
...
</code></pre></div></div>

<h2 id="environment-variables">Environment Variables</h2>

<p>The way you use <code class="language-plaintext highlighter-rouge">ENV</code> is by defining it inside the stage (a.k.a Local Scope).</p>

<p>The benefit of using <code class="language-plaintext highlighter-rouge">ENV</code> is we can configure the way our container run.</p>

<p>The value of <code class="language-plaintext highlighter-rouge">ENV</code> available during build process and when the image is running as container.</p>

<p>I recommend using <code class="language-plaintext highlighter-rouge">ENV</code> for configuring runtime. For example, to set API endpoints. I don’t recommend using it for sensitive values like passwords.</p>

<p>To pass environment variables during build, we need to use build arguments. This is why I previously said they are closely related.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> USER</span>

<span class="k">FROM</span><span class="s"> golang:1.23</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> &lt;&lt;EOF ./main.go</span>
package main

import (
  "fmt"
  "os"
)

func main() {
  fmt.Println("Hello", os.Getenv("USER_NAME"))
}
EOF
<span class="k">RUN </span>go build <span class="nt">-o</span> /bin/hello ./main.go

<span class="k">FROM</span><span class="s"> scratch</span>
<span class="c"># inherit &amp; copy the arg from global scope</span>
<span class="k">ARG</span><span class="s"> USER </span>
<span class="k">ENV</span><span class="s"> USER_NAME=${USER}</span>
<span class="k">COPY</span><span class="s"> --from=0 /bin/hello /bin/hello</span>
<span class="k">CMD</span><span class="s"> ["/bin/hello"]</span>
</code></pre></div></div>

<p>Now, after building it, the environment variables will be available in the container, and they can be accessed programmatically (<code class="language-plaintext highlighter-rouge">os.Getenv("ENV_NAME")</code> in Go or <code class="language-plaintext highlighter-rouge">process.env.ENV_NAME</code> in Node.js).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">--build-arg</span> <span class="nv">USER</span><span class="o">=</span><span class="s2">"example"</span> <span class="nt">-t</span> example:latest <span class="nb">.</span>
</code></pre></div></div>

<p>If you are wondering why the <code class="language-plaintext highlighter-rouge">USER_NAME</code> is called by the code in the first stage (build), but the environment variable is copied to the second stage (run), I have an answer for you.</p>

<p>This is because the first stage does not need the environment variables to be present; it will still compile. Once compiled, the binary runs in the second stage, which is where we want the environment variables to be present. Hence, we copied them to the second stage and not the first stage.</p>

<h2 id="build-argument-vs-environment-variables">Build Argument vs Environment Variables</h2>

<p>So here’s a TL;DR version of Build Arguments vs Environment Variables.</p>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Build Arguments (ARG)</th>
      <th>Environment Variables (ENV)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Declaration</td>
      <td>Using <code class="language-plaintext highlighter-rouge">ARG</code> keyword</td>
      <td>Using <code class="language-plaintext highlighter-rouge">ENV</code> keyword</td>
    </tr>
    <tr>
      <td>Scope</td>
      <td>Can be Global or Local (stage)</td>
      <td>Local (stage) only</td>
    </tr>
    <tr>
      <td>Availability</td>
      <td>Only during build time (unless explicitly passed)</td>
      <td>During build time and container runtime</td>
    </tr>
    <tr>
      <td>Best Used For</td>
      <td>Build configurations (versions, base images)</td>
      <td>Runtime configurations (API endpoints)</td>
    </tr>
  </tbody>
</table>

<h2 id="build-secrets">Build Secrets</h2>

<p>Why would there be build secrets when build arguments and environment variables already exist?</p>

<p>Well, they are for storing sensitive information like passwords or API keys. By using build secrets, the sensitive information will not be exposed.</p>

<p>Let’s take an example of passing an API key when building a frontend app in JavaScript:</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> SECRET_API_KEY</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">node:20</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">build</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>

<span class="k">RUN </span>npm <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">ARG</span><span class="s"> SECRET_API_KEY</span>
<span class="k">RUN </span><span class="nv">SECRET_API_KEY</span><span class="o">=</span><span class="k">${</span><span class="nv">SECRET_API_KEY</span><span class="k">}</span> <span class="se">\
</span>    npm run build

<span class="k">FROM</span><span class="s"> node:20</span>
<span class="k">RUN </span>npm <span class="nb">install</span> <span class="nt">-g</span> serve
<span class="k">COPY</span><span class="s"> --from=build /app/dist ./dist</span>
<span class="k">EXPOSE</span><span class="s"> 3000</span>
<span class="k">CMD</span><span class="s"> ["serve", "-s", "dist", "-l", "3000"]</span>
</code></pre></div></div>

<p>And build it by passing the secret using an argument:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">--build-arg</span> <span class="nv">SECRET_API_KEY</span><span class="o">=</span><span class="s2">"example"</span> <span class="nt">-t</span> example:latest <span class="nb">.</span>
</code></pre></div></div>

<p>Yeah, it did work with build arguments, and even if we pass that argument to an environment variable before using it in the build, it will still work.</p>

<p>The problem is we are leaking sensitive information. Docker will also print a warning message about this.</p>

<p>Let’s change the implementation to use build secrets.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>

<span class="k">RUN </span>npm <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span><span class="nt">--mount</span><span class="o">=</span><span class="nb">type</span><span class="o">=</span>secret,id<span class="o">=</span>SECRET_API_KEY <span class="se">\
</span>    <span class="nv">SECRET_API_KEY</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /run/secrets/SECRET_API_KEY<span class="si">)</span> <span class="se">\
</span>    npm run build

<span class="k">FROM</span><span class="s"> node:20-alpine</span>
<span class="k">RUN </span>npm <span class="nb">install</span> <span class="nt">-g</span> serve
<span class="k">COPY</span><span class="s"> --from=build /app/dist ./dist</span>
<span class="k">EXPOSE</span><span class="s"> 3000</span>
<span class="k">CMD</span><span class="s"> ["serve", "-s", "dist", "-l", "3000"]</span>
</code></pre></div></div>

<p>We will also change the build command and add the environment variable to the shell before executing the build command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export SECRET_API_KEY=example
docker build --secret id=SECRET_API_KEY -t example:latest .
</code></pre></div></div>

<p>No more warning messages, and we successfully pass the secret to the build. Yay!</p>

<h2 id="environment-variables-vs-build-secrets">Environment Variables vs Build Secrets</h2>

<p>So here’s a TL;DR version of Environment Variables vs Build Secrets.</p>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Environment Variables (ENV)</th>
      <th>Build Secrets</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Declaration</td>
      <td>Using <code class="language-plaintext highlighter-rouge">ENV</code> keyword</td>
      <td>Using <code class="language-plaintext highlighter-rouge">--mount=type=secret</code></td>
    </tr>
    <tr>
      <td>Visibility</td>
      <td>Visible in image history and container</td>
      <td>Not visible in image history</td>
    </tr>
    <tr>
      <td>Security Level</td>
      <td>Lower (plaintext)</td>
      <td>Higher (secure during build)</td>
    </tr>
    <tr>
      <td>Best Used For</td>
      <td>Non-sensitive configuration</td>
      <td>Sensitive data (API keys, credentials)</td>
    </tr>
  </tbody>
</table>

<hr />

<p>Photo by <a href="https://unsplash.com/@carrier_lost?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Ian Taylor on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Development" /><summary type="html"><![CDATA[Learn the differences between Docker build arguments, environment variables, and build secrets. Understand when and how to use each for configuring your Docker builds and containers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2024-12-07-docker-argument-vs-variable-vs-secret.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2024-12-07-docker-argument-vs-variable-vs-secret.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dealing With Rejections</title><link href="https://madeindra.dev/dealing-with-rejections/" rel="alternate" type="text/html" title="Dealing With Rejections" /><published>2024-11-24T00:00:00+00:00</published><updated>2024-11-24T00:00:00+00:00</updated><id>https://madeindra.dev/dealing-with-rejections</id><content type="html" xml:base="https://madeindra.dev/dealing-with-rejections/"><![CDATA[<p>You are in dire need of a job, and months in, you still haven’t found the right one. You wake up to an email saying they are proceeding with other candidates that better fit their needs, and it hurts. It hurts your ego, it hurts your pride; in short, it hurts. No matter how much you have grown, rejection always hurts. The worst thing about rejection is that you can never get used to it.</p>

<p>Rejection does not exclusively come from your job hunt; it can occur in any aspect of your life. Confessing your feelings, joining a new community, sharing your ideas in public, or even opening up to your own family can lead to rejection. One thing I’ve realized is that even when it’s not our identity that’s being rejected, still, we associate that rejection with ourselves. It’s already hurting us even when we’re only imagining it.</p>

<p>I’m not going to write about how to handle rejections; in fact, I’m not even sure how to do it myself.</p>

<p>The feeling of being worthless, it keeps eating at your heart. You sink even deeper into sadness. It feels like whatever you do doesn’t matter anymore. Sometimes there’s also anger that’s being fueled up by denials.</p>

<p>I know full well how it feels. Like you, I’ve been through a lot of rejections too.</p>

<p>I want to tell you this: whether you didn’t get the job, didn’t win the prize, or had your idea dismissed, it does not change your value.</p>

<p>When you start to feel like you’re in the dumpster, take a rest. Take your time—no matter how long it takes—take as much as you need. I know it sounds cliché, but time does heal wounds. Do things you enjoy, and involve yourself with people who matter. One day, when you’re ready to bounce back, you’ll do it better because you’ve learned the lessons from the rejections you faced.</p>

<p>I pray that one day you will succeed.</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@a_d_s_w?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Adrian Swancar on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Life" /><summary type="html"><![CDATA[Rejection is never easy—whether it’s a job you didn’t land or a competition you didn't win, it stings.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2024-11-24-dealing-with-rejection.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2024-11-24-dealing-with-rejection.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tech Stack to Learn for Beginner</title><link href="https://madeindra.dev/tech-stack-to-learn-for-beginner/" rel="alternate" type="text/html" title="Tech Stack to Learn for Beginner" /><published>2024-10-20T00:00:00+00:00</published><updated>2024-10-20T00:00:00+00:00</updated><id>https://madeindra.dev/tech-stack-to-learn-for-beginner</id><content type="html" xml:base="https://madeindra.dev/tech-stack-to-learn-for-beginner/"><![CDATA[<p>We all know how saturated the current tech market is. After the Covid-19 pandemic came to pass, several waves of layoff have occured, and many tech companies are now calling for return to the office. Those caused the market to get even more chaotic. Initially, the market were full of people transitioning from non-tech careers, but now, it’s flooded with experienced people who were either laid off or are unwilling to return to in-office setting. Even worse, companies are not looking to hire more junior candidate anymore, they have less and less chance.</p>

<p>In the midst of this madness, one potential way out of is to explore opportunities with overseas companies, whether through remote work or relocation.</p>

<p>Here’s the thing, standing out among the crowd is not easy. I’m not going to cover resume-polishing tips, those were already widely discussed. Instead, I will focus on which tech stack could give you an edge if you are a newcomer. Bear in mind that these are based on my own observation.</p>

<p>For the backend development, Java and Phython are the most commonly listed programming language in job postings. Moreover, these two has been around for decades, so a lot of technologies are already built on them. Fot the frameworks, it’s usually Java with Spring Boot and Python with either Django or a more recent FastAPI. When it comes to databases, there are typically 2 categories: SQL and NoSQL. For sure, if it’s SQL, the most in-demand would be PostgreSQL or MySQL, but for NoSQL it usually depends on the product they are working on, it could be MongoDB, Cassandra, or even Neo4j. Lastly, familiarity with cloud platforms such as AWS, GCP, or Azure is essential.</p>

<p>For the frontend development, React is on the top. By itself, to master React you need master HTML, CSS, and JavaScript. And, yes, you can start from JavaScript, but skipping TypeScript is not an option in today’s market. Also, if you want to stick to one language for the whole fullstack development (both backend and frontend), you can accompany React with NestJS.</p>

<p>I am not well-versed in roles related to Data, AI, DevOps, SRE, or similar fields, so I can’t give much insight about those area.</p>

<p>However, for mobile development, I recommend to stick with native platforms rather than hybrid like React Native or Flutter. I’ve noticed in several postings that the requirement specify iOS with Swift and Android with Kotlin.</p>

<p>Now, where can you find the postings?</p>

<p>For remote job opportunities, you can start by checking out platforms like <a href="https://remote.com/jobs">Remote.com</a>, <a href="https://remotive.com/">Remotive</a>, <a href="https://wellfound.com/jobs">Wellfound</a>, <a href="https://weworkremotely.com/">WeWorkRemotely</a> and <a href="https://jobgether.com">Jobgether</a>.</p>

<p>When it comes to jobs offering relocation support, I can only think of <a href="https://relocate.me/">Relocate.me</a> as the most trusted. Many companies also list relocation as part of th their perk, so be sure to keep an eye on their career pages.</p>

<hr />

<p>Photo by <a href="https://unsplash.com/@grakozy?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Greg Rakozy on Unsplash</a></p>]]></content><author><name>Made Indra</name></author><category term="Learning" /><summary type="html"><![CDATA[Let's explore the current competitive tech job market, with tips on popular tech stacks and where to find remotes and relocation opportunities.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://madeindra.dev/assets/images/2024-10-20-tech-stack-to-learn-for-beginner.webp" /><media:content medium="image" url="https://madeindra.dev/assets/images/2024-10-20-tech-stack-to-learn-for-beginner.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>